/* eslint-disable */
;(function(global, factory) {
  global['Long'] = factory()
})(this, function() {
  'use strict'

  function Long(low, high, unsigned) {
    this.low = low | 0
    this.high = high | 0
    this.unsigned = !!unsigned
  }
  Long.prototype.__isLong__

  Object.defineProperty(Long.prototype, '__isLong__', {
    value: true,
    enumerable: false,
    configurable: false
  })

  function isLong(obj) {
    return (obj && obj['__isLong__']) === true
  }
  Long.isLong = isLong
  var INT_CACHE = {}
  var UINT_CACHE = {}

  function fromInt(value, unsigned) {
    var obj, cachedObj, cache
    if (unsigned) {
      value >>>= 0
      if ((cache = 0 <= value && value < 256)) {
        cachedObj = UINT_CACHE[value]
        if (cachedObj) return cachedObj
      }
      obj = fromBits(value, (value | 0) < 0 ? -1 : 0, true)
      if (cache) UINT_CACHE[value] = obj
      return obj
    } else {
      value |= 0
      if ((cache = -128 <= value && value < 128)) {
        cachedObj = INT_CACHE[value]
        if (cachedObj) return cachedObj
      }
      obj = fromBits(value, value < 0 ? -1 : 0, false)
      if (cache) INT_CACHE[value] = obj
      return obj
    }
  }
  Long.fromInt = fromInt

  function fromNumber(value, unsigned) {
    if (isNaN(value) || !isFinite(value)) return unsigned ? UZERO : ZERO
    if (unsigned) {
      if (value < 0) return UZERO
      if (value >= TWO_PWR_64_DBL) return MAX_UNSIGNED_VALUE
    } else {
      if (value <= -TWO_PWR_63_DBL) return MIN_VALUE
      if (value + 1 >= TWO_PWR_63_DBL) return MAX_VALUE
    }
    if (value < 0) return fromNumber(-value, unsigned).neg()
    return fromBits(value % TWO_PWR_32_DBL | 0, (value / TWO_PWR_32_DBL) | 0, unsigned)
  }
  Long.fromNumber = fromNumber

  function fromBits(lowBits, highBits, unsigned) {
    return new Long(lowBits, highBits, unsigned)
  }
  Long.fromBits = fromBits
  var pow_dbl = Math.pow // Used 4 times (4*8 to 15+4)

  function fromString(str, unsigned, radix) {
    if (str.length === 0) throw Error('empty string')
    if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') return ZERO
    if (typeof unsigned === 'number') {
      // For goog.math.long compatibility
      ;(radix = unsigned), (unsigned = false)
    } else {
      unsigned = !!unsigned
    }
    radix = radix || 10
    if (radix < 2 || 36 < radix) throw RangeError('radix')

    var p
    if ((p = str.indexOf('-')) > 0) throw Error('interior hyphen')
    else if (p === 0) {
      return fromString(str.substring(1), unsigned, radix).neg()
    }
    var radixToPower = fromNumber(pow_dbl(radix, 8))

    var result = ZERO
    for (var i = 0; i < str.length; i += 8) {
      var size = Math.min(8, str.length - i),
        value = parseInt(str.substring(i, i + size), radix)
      if (size < 8) {
        var power = fromNumber(pow_dbl(radix, size))
        result = result.mul(power).add(fromNumber(value))
      } else {
        result = result.mul(radixToPower)
        result = result.add(fromNumber(value))
      }
    }
    result.unsigned = unsigned
    return result
  }
  Long.fromString = fromString

  function fromValue(val) {
    if (val /* is compatible */ instanceof Long) return val
    if (typeof val === 'number') return fromNumber(val)
    if (typeof val === 'string') return fromString(val)
    // Throws for non-objects, converts non-instanceof Long:
    return fromBits(val.low, val.high, val.unsigned)
  }
  Long.fromValue = fromValue
  var TWO_PWR_16_DBL = 1 << 16
  var TWO_PWR_24_DBL = 1 << 24
  var TWO_PWR_32_DBL = TWO_PWR_16_DBL * TWO_PWR_16_DBL
  var TWO_PWR_64_DBL = TWO_PWR_32_DBL * TWO_PWR_32_DBL
  var TWO_PWR_63_DBL = TWO_PWR_64_DBL / 2
  var TWO_PWR_24 = fromInt(TWO_PWR_24_DBL)
  var ZERO = fromInt(0)
  Long.ZERO = ZERO
  var UZERO = fromInt(0, true)
  Long.UZERO = UZERO
  var ONE = fromInt(1)
  Long.ONE = ONE
  var UONE = fromInt(1, true)
  Long.UONE = UONE
  var NEG_ONE = fromInt(-1)
  Long.NEG_ONE = NEG_ONE
  var MAX_VALUE = fromBits(0xffffffff | 0, 0x7fffffff | 0, false)
  Long.MAX_VALUE = MAX_VALUE
  var MAX_UNSIGNED_VALUE = fromBits(0xffffffff | 0, 0xffffffff | 0, true)
  Long.MAX_UNSIGNED_VALUE = MAX_UNSIGNED_VALUE
  var MIN_VALUE = fromBits(0, 0x80000000 | 0, false)
  Long.MIN_VALUE = MIN_VALUE
  var LongPrototype = Long.prototype
  LongPrototype.toInt = function toInt() {
    return this.unsigned ? this.low >>> 0 : this.low
  }
  LongPrototype.toNumber = function toNumber() {
    if (this.unsigned) return (this.high >>> 0) * TWO_PWR_32_DBL + (this.low >>> 0)
    return this.high * TWO_PWR_32_DBL + (this.low >>> 0)
  }

  LongPrototype.toString = function toString(radix) {
    radix = radix || 10
    if (radix < 2 || 36 < radix) throw RangeError('radix')
    if (this.isZero()) return '0'
    if (this.isNegative()) {
      // Unsigned Longs are never negative
      if (this.eq(MIN_VALUE)) {
        // We need to change the Long value before it can be negated, so we remove
        // the bottom-most digit in this base and then recurse to do the rest.
        var radixLong = fromNumber(radix),
          div = this.div(radixLong),
          rem1 = div.mul(radixLong).sub(this)
        return div.toString(radix) + rem1.toInt().toString(radix)
      } else return '-' + this.neg().toString(radix)
    }
    var radixToPower = fromNumber(pow_dbl(radix, 6), this.unsigned),
      rem = this
    var result = ''
    while (true) {
      var remDiv = rem.div(radixToPower),
        intval = rem.sub(remDiv.mul(radixToPower)).toInt() >>> 0,
        digits = intval.toString(radix)
      rem = remDiv
      if (rem.isZero()) return digits + result
      else {
        while (digits.length < 6) digits = '0' + digits
        result = '' + digits + result
      }
    }
  }
  LongPrototype.getHighBits = function getHighBits() {
    return this.high
  }
  LongPrototype.getHighBitsUnsigned = function getHighBitsUnsigned() {
    return this.high >>> 0
  }
  LongPrototype.getLowBits = function getLowBits() {
    return this.low
  }
  LongPrototype.getLowBitsUnsigned = function getLowBitsUnsigned() {
    return this.low >>> 0
  }
  LongPrototype.getNumBitsAbs = function getNumBitsAbs() {
    if (this.isNegative())
      // Unsigned Longs are never negative
      return this.eq(MIN_VALUE) ? 64 : this.neg().getNumBitsAbs()
    var val = this.high != 0 ? this.high : this.low
    for (var bit = 31; bit > 0; bit--) if ((val & (1 << bit)) != 0) break
    return this.high != 0 ? bit + 33 : bit + 1
  }
  LongPrototype.isZero = function isZero() {
    return this.high === 0 && this.low === 0
  }
  LongPrototype.isNegative = function isNegative() {
    return !this.unsigned && this.high < 0
  }
  LongPrototype.isPositive = function isPositive() {
    return this.unsigned || this.high >= 0
  }
  LongPrototype.isOdd = function isOdd() {
    return (this.low & 1) === 1
  }
  LongPrototype.isEven = function isEven() {
    return (this.low & 1) === 0
  }
  LongPrototype.equals = function equals(other) {
    if (!isLong(other)) other = fromValue(other)
    if (this.unsigned !== other.unsigned && this.high >>> 31 === 1 && other.high >>> 31 === 1) return false
    return this.high === other.high && this.low === other.low
  }
  LongPrototype.eq = LongPrototype.equals
  LongPrototype.notEquals = function notEquals(other) {
    return !this.eq(/* validates */ other)
  }
  LongPrototype.neq = LongPrototype.notEquals

  /**
   * Tests if this Long's value is less than the specified's.
   * @param {!Long|number|string} other Other value
   * @returns {boolean}
   */
  LongPrototype.lessThan = function lessThan(other) {
    return this.comp(/* validates */ other) < 0
  }

  /**
   * Tests if this Long's value is less than the specified's. This is an alias of {@link Long#lessThan}.
   * @function
   * @param {!Long|number|string} other Other value
   * @returns {boolean}
   */
  LongPrototype.lt = LongPrototype.lessThan

  /**
   * Tests if this Long's value is less than or equal the specified's.
   * @param {!Long|number|string} other Other value
   * @returns {boolean}
   */
  LongPrototype.lessThanOrEqual = function lessThanOrEqual(other) {
    return this.comp(/* validates */ other) <= 0
  }

  /**
   * Tests if this Long's value is less than or equal the specified's. This is an alias of {@link Long#lessThanOrEqual}.
   * @function
   * @param {!Long|number|string} other Other value
   * @returns {boolean}
   */
  LongPrototype.lte = LongPrototype.lessThanOrEqual

  /**
   * Tests if this Long's value is greater than the specified's.
   * @param {!Long|number|string} other Other value
   * @returns {boolean}
   */
  LongPrototype.greaterThan = function greaterThan(other) {
    return this.comp(/* validates */ other) > 0
  }

  /**
   * Tests if this Long's value is greater than the specified's. This is an alias of {@link Long#greaterThan}.
   * @function
   * @param {!Long|number|string} other Other value
   * @returns {boolean}
   */
  LongPrototype.gt = LongPrototype.greaterThan

  /**
   * Tests if this Long's value is greater than or equal the specified's.
   * @param {!Long|number|string} other Other value
   * @returns {boolean}
   */
  LongPrototype.greaterThanOrEqual = function greaterThanOrEqual(other) {
    return this.comp(/* validates */ other) >= 0
  }

  /**
   * Tests if this Long's value is greater than or equal the specified's. This is an alias of {@link Long#greaterThanOrEqual}.
   * @function
   * @param {!Long|number|string} other Other value
   * @returns {boolean}
   */
  LongPrototype.gte = LongPrototype.greaterThanOrEqual

  /**
   * Compares this Long's value with the specified's.
   * @param {!Long|number|string} other Other value
   * @returns {number} 0 if they are the same, 1 if the this is greater and -1
   *  if the given one is greater
   */
  LongPrototype.compare = function compare(other) {
    if (!isLong(other)) other = fromValue(other)
    if (this.eq(other)) return 0
    var thisNeg = this.isNegative(),
      otherNeg = other.isNegative()
    if (thisNeg && !otherNeg) return -1
    if (!thisNeg && otherNeg) return 1
    // At this point the sign bits are the same
    if (!this.unsigned) return this.sub(other).isNegative() ? -1 : 1
    // Both are positive if at least one is unsigned
    return other.high >>> 0 > this.high >>> 0 || (other.high === this.high && other.low >>> 0 > this.low >>> 0) ? -1 : 1
  }

  /**
   * Compares this Long's value with the specified's. This is an alias of {@link Long#compare}.
   * @function
   * @param {!Long|number|string} other Other value
   * @returns {number} 0 if they are the same, 1 if the this is greater and -1
   *  if the given one is greater
   */
  LongPrototype.comp = LongPrototype.compare

  /**
   * Negates this Long's value.
   * @returns {!Long} Negated Long
   */
  LongPrototype.negate = function negate() {
    if (!this.unsigned && this.eq(MIN_VALUE)) return MIN_VALUE
    return this.not().add(ONE)
  }

  /**
   * Negates this Long's value. This is an alias of {@link Long#negate}.
   * @function
   * @returns {!Long} Negated Long
   */
  LongPrototype.neg = LongPrototype.negate

  /**
   * Returns the sum of this and the specified Long.
   * @param {!Long|number|string} addend Addend
   * @returns {!Long} Sum
   */
  LongPrototype.add = function add(addend) {
    if (!isLong(addend)) addend = fromValue(addend)

    // Divide each number into 4 chunks of 16 bits, and then sum the chunks.

    var a48 = this.high >>> 16
    var a32 = this.high & 0xffff
    var a16 = this.low >>> 16
    var a00 = this.low & 0xffff

    var b48 = addend.high >>> 16
    var b32 = addend.high & 0xffff
    var b16 = addend.low >>> 16
    var b00 = addend.low & 0xffff

    var c48 = 0,
      c32 = 0,
      c16 = 0,
      c00 = 0
    c00 += a00 + b00
    c16 += c00 >>> 16
    c00 &= 0xffff
    c16 += a16 + b16
    c32 += c16 >>> 16
    c16 &= 0xffff
    c32 += a32 + b32
    c48 += c32 >>> 16
    c32 &= 0xffff
    c48 += a48 + b48
    c48 &= 0xffff
    return fromBits((c16 << 16) | c00, (c48 << 16) | c32, this.unsigned)
  }

  /**
   * Returns the difference of this and the specified Long.
   * @param {!Long|number|string} subtrahend Subtrahend
   * @returns {!Long} Difference
   */
  LongPrototype.subtract = function subtract(subtrahend) {
    if (!isLong(subtrahend)) subtrahend = fromValue(subtrahend)
    return this.add(subtrahend.neg())
  }

  /**
   * Returns the difference of this and the specified Long. This is an alias of {@link Long#subtract}.
   * @function
   * @param {!Long|number|string} subtrahend Subtrahend
   * @returns {!Long} Difference
   */
  LongPrototype.sub = LongPrototype.subtract

  /**
   * Returns the product of this and the specified Long.
   * @param {!Long|number|string} multiplier Multiplier
   * @returns {!Long} Product
   */
  LongPrototype.multiply = function multiply(multiplier) {
    if (this.isZero()) return ZERO
    if (!isLong(multiplier)) multiplier = fromValue(multiplier)
    if (multiplier.isZero()) return ZERO
    if (this.eq(MIN_VALUE)) return multiplier.isOdd() ? MIN_VALUE : ZERO
    if (multiplier.eq(MIN_VALUE)) return this.isOdd() ? MIN_VALUE : ZERO

    if (this.isNegative()) {
      if (multiplier.isNegative()) return this.neg().mul(multiplier.neg())
      else
        return this.neg()
          .mul(multiplier)
          .neg()
    } else if (multiplier.isNegative()) return this.mul(multiplier.neg()).neg()

    // If both longs are small, use float multiplication
    if (this.lt(TWO_PWR_24) && multiplier.lt(TWO_PWR_24))
      return fromNumber(this.toNumber() * multiplier.toNumber(), this.unsigned)

    // Divide each long into 4 chunks of 16 bits, and then add up 4x4 products.
    // We can skip products that would overflow.

    var a48 = this.high >>> 16
    var a32 = this.high & 0xffff
    var a16 = this.low >>> 16
    var a00 = this.low & 0xffff

    var b48 = multiplier.high >>> 16
    var b32 = multiplier.high & 0xffff
    var b16 = multiplier.low >>> 16
    var b00 = multiplier.low & 0xffff

    var c48 = 0,
      c32 = 0,
      c16 = 0,
      c00 = 0
    c00 += a00 * b00
    c16 += c00 >>> 16
    c00 &= 0xffff
    c16 += a16 * b00
    c32 += c16 >>> 16
    c16 &= 0xffff
    c16 += a00 * b16
    c32 += c16 >>> 16
    c16 &= 0xffff
    c32 += a32 * b00
    c48 += c32 >>> 16
    c32 &= 0xffff
    c32 += a16 * b16
    c48 += c32 >>> 16
    c32 &= 0xffff
    c32 += a00 * b32
    c48 += c32 >>> 16
    c32 &= 0xffff
    c48 += a48 * b00 + a32 * b16 + a16 * b32 + a00 * b48
    c48 &= 0xffff
    return fromBits((c16 << 16) | c00, (c48 << 16) | c32, this.unsigned)
  }

  /**
   * Returns the product of this and the specified Long. This is an alias of {@link Long#multiply}.
   * @function
   * @param {!Long|number|string} multiplier Multiplier
   * @returns {!Long} Product
   */
  LongPrototype.mul = LongPrototype.multiply

  /**
   * Returns this Long divided by the specified. The result is signed if this Long is signed or
   *  unsigned if this Long is unsigned.
   * @param {!Long|number|string} divisor Divisor
   * @returns {!Long} Quotient
   */
  LongPrototype.divide = function divide(divisor) {
    if (!isLong(divisor)) divisor = fromValue(divisor)
    if (divisor.isZero()) throw Error('division by zero')
    if (this.isZero()) return this.unsigned ? UZERO : ZERO
    var approx, rem, res
    if (!this.unsigned) {
      // This section is only relevant for signed longs and is derived from the
      // closure library as a whole.
      if (this.eq(MIN_VALUE)) {
        if (divisor.eq(ONE) || divisor.eq(NEG_ONE)) return MIN_VALUE
        // recall that -MIN_VALUE == MIN_VALUE
        else if (divisor.eq(MIN_VALUE)) return ONE
        else {
          // At this point, we have |other| >= 2, so |this/other| < |MIN_VALUE|.
          var halfThis = this.shr(1)
          approx = halfThis.div(divisor).shl(1)
          if (approx.eq(ZERO)) {
            return divisor.isNegative() ? ONE : NEG_ONE
          } else {
            rem = this.sub(divisor.mul(approx))
            res = approx.add(rem.div(divisor))
            return res
          }
        }
      } else if (divisor.eq(MIN_VALUE)) return this.unsigned ? UZERO : ZERO
      if (this.isNegative()) {
        if (divisor.isNegative()) return this.neg().div(divisor.neg())
        return this.neg()
          .div(divisor)
          .neg()
      } else if (divisor.isNegative()) return this.div(divisor.neg()).neg()
      res = ZERO
    } else {
      // The algorithm below has not been made for unsigned longs. It's therefore
      // required to take special care of the MSB prior to running it.
      if (!divisor.unsigned) divisor = divisor.toUnsigned()
      if (divisor.gt(this)) return UZERO
      if (divisor.gt(this.shru(1)))
        // 15 >>> 1 = 7 ; with divisor = 8 ; true
        return UONE
      res = UZERO
    }

    // Repeat the following until the remainder is less than other:  find a
    // floating-point that approximates remainder / other *from below*, add this
    // into the result, and subtract it from the remainder.  It is critical that
    // the approximate value is less than or equal to the real value so that the
    // remainder never becomes negative.
    rem = this
    while (rem.gte(divisor)) {
      // Approximate the result of division. This may be a little greater or
      // smaller than the actual value.
      approx = Math.max(1, Math.floor(rem.toNumber() / divisor.toNumber()))

      // We will tweak the approximate result by changing it in the 48-th digit or
      // the smallest non-fractional digit, whichever is larger.
      var log2 = Math.ceil(Math.log(approx) / Math.LN2),
        delta = log2 <= 48 ? 1 : pow_dbl(2, log2 - 48),
        // Decrease the approximation until it is smaller than the remainder.  Note
        // that if it is too large, the product overflows and is negative.
        approxRes = fromNumber(approx),
        approxRem = approxRes.mul(divisor)
      while (approxRem.isNegative() || approxRem.gt(rem)) {
        approx -= delta
        approxRes = fromNumber(approx, this.unsigned)
        approxRem = approxRes.mul(divisor)
      }

      // We know the answer can't be zero... and actually, zero would cause
      // infinite recursion since we would make no progress.
      if (approxRes.isZero()) approxRes = ONE

      res = res.add(approxRes)
      rem = rem.sub(approxRem)
    }
    return res
  }

  /**
   * Returns this Long divided by the specified. This is an alias of {@link Long#divide}.
   * @function
   * @param {!Long|number|string} divisor Divisor
   * @returns {!Long} Quotient
   */
  LongPrototype.div = LongPrototype.divide

  /**
   * Returns this Long modulo the specified.
   * @param {!Long|number|string} divisor Divisor
   * @returns {!Long} Remainder
   */
  LongPrototype.modulo = function modulo(divisor) {
    if (!isLong(divisor)) divisor = fromValue(divisor)
    return this.sub(this.div(divisor).mul(divisor))
  }

  /**
   * Returns this Long modulo the specified. This is an alias of {@link Long#modulo}.
   * @function
   * @param {!Long|number|string} divisor Divisor
   * @returns {!Long} Remainder
   */
  LongPrototype.mod = LongPrototype.modulo

  /**
   * Returns the bitwise NOT of this Long.
   * @returns {!Long}
   */
  LongPrototype.not = function not() {
    return fromBits(~this.low, ~this.high, this.unsigned)
  }

  /**
   * Returns the bitwise AND of this Long and the specified.
   * @param {!Long|number|string} other Other Long
   * @returns {!Long}
   */
  LongPrototype.and = function and(other) {
    if (!isLong(other)) other = fromValue(other)
    return fromBits(this.low & other.low, this.high & other.high, this.unsigned)
  }

  /**
   * Returns the bitwise OR of this Long and the specified.
   * @param {!Long|number|string} other Other Long
   * @returns {!Long}
   */
  LongPrototype.or = function or(other) {
    if (!isLong(other)) other = fromValue(other)
    return fromBits(this.low | other.low, this.high | other.high, this.unsigned)
  }

  /**
   * Returns the bitwise XOR of this Long and the given one.
   * @param {!Long|number|string} other Other Long
   * @returns {!Long}
   */
  LongPrototype.xor = function xor(other) {
    if (!isLong(other)) other = fromValue(other)
    return fromBits(this.low ^ other.low, this.high ^ other.high, this.unsigned)
  }

  /**
   * Returns this Long with bits shifted to the left by the given amount.
   * @param {number|!Long} numBits Number of bits
   * @returns {!Long} Shifted Long
   */
  LongPrototype.shiftLeft = function shiftLeft(numBits) {
    if (isLong(numBits)) numBits = numBits.toInt()
    if ((numBits &= 63) === 0) return this
    else if (numBits < 32)
      return fromBits(this.low << numBits, (this.high << numBits) | (this.low >>> (32 - numBits)), this.unsigned)
    else return fromBits(0, this.low << (numBits - 32), this.unsigned)
  }

  /**
   * Returns this Long with bits shifted to the left by the given amount. This is an alias of {@link Long#shiftLeft}.
   * @function
   * @param {number|!Long} numBits Number of bits
   * @returns {!Long} Shifted Long
   */
  LongPrototype.shl = LongPrototype.shiftLeft

  /**
   * Returns this Long with bits arithmetically shifted to the right by the given amount.
   * @param {number|!Long} numBits Number of bits
   * @returns {!Long} Shifted Long
   */
  LongPrototype.shiftRight = function shiftRight(numBits) {
    if (isLong(numBits)) numBits = numBits.toInt()
    if ((numBits &= 63) === 0) return this
    else if (numBits < 32)
      return fromBits((this.low >>> numBits) | (this.high << (32 - numBits)), this.high >> numBits, this.unsigned)
    else return fromBits(this.high >> (numBits - 32), this.high >= 0 ? 0 : -1, this.unsigned)
  }

  /**
   * Returns this Long with bits arithmetically shifted to the right by the given amount. This is an alias of {@link Long#shiftRight}.
   * @function
   * @param {number|!Long} numBits Number of bits
   * @returns {!Long} Shifted Long
   */
  LongPrototype.shr = LongPrototype.shiftRight

  /**
   * Returns this Long with bits logically shifted to the right by the given amount.
   * @param {number|!Long} numBits Number of bits
   * @returns {!Long} Shifted Long
   */
  LongPrototype.shiftRightUnsigned = function shiftRightUnsigned(numBits) {
    if (isLong(numBits)) numBits = numBits.toInt()
    numBits &= 63
    if (numBits === 0) return this
    else {
      var high = this.high
      if (numBits < 32) {
        var low = this.low
        return fromBits((low >>> numBits) | (high << (32 - numBits)), high >>> numBits, this.unsigned)
      } else if (numBits === 32) return fromBits(high, 0, this.unsigned)
      else return fromBits(high >>> (numBits - 32), 0, this.unsigned)
    }
  }

  /**
   * Returns this Long with bits logically shifted to the right by the given amount. This is an alias of {@link Long#shiftRightUnsigned}.
   * @function
   * @param {number|!Long} numBits Number of bits
   * @returns {!Long} Shifted Long
   */
  LongPrototype.shru = LongPrototype.shiftRightUnsigned

  /**
   * Converts this Long to signed.
   * @returns {!Long} Signed long
   */
  LongPrototype.toSigned = function toSigned() {
    if (!this.unsigned) return this
    return fromBits(this.low, this.high, false)
  }

  /**
   * Converts this Long to unsigned.
   * @returns {!Long} Unsigned long
   */
  LongPrototype.toUnsigned = function toUnsigned() {
    if (this.unsigned) return this
    return fromBits(this.low, this.high, true)
  }

  /**
   * Converts this Long to its byte representation.
   * @param {boolean=} le Whether little or big endian, defaults to big endian
   * @returns {!Array.<number>} Byte representation
   */
  LongPrototype.toBytes = function(le) {
    return le ? this.toBytesLE() : this.toBytesBE()
  }

  /**
   * Converts this Long to its little endian byte representation.
   * @returns {!Array.<number>} Little endian byte representation
   */
  LongPrototype.toBytesLE = function() {
    var hi = this.high,
      lo = this.low
    return [
      lo & 0xff,
      (lo >>> 8) & 0xff,
      (lo >>> 16) & 0xff,
      (lo >>> 24) & 0xff,
      hi & 0xff,
      (hi >>> 8) & 0xff,
      (hi >>> 16) & 0xff,
      (hi >>> 24) & 0xff
    ]
  }

  /**
   * Converts this Long to its big endian byte representation.
   * @returns {!Array.<number>} Big endian byte representation
   */
  LongPrototype.toBytesBE = function() {
    var hi = this.high,
      lo = this.low
    return [
      (hi >>> 24) & 0xff,
      (hi >>> 16) & 0xff,
      (hi >>> 8) & 0xff,
      hi & 0xff,
      (lo >>> 24) & 0xff,
      (lo >>> 16) & 0xff,
      (lo >>> 8) & 0xff,
      lo & 0xff
    ]
  }

  return Long
})

let Long = this.Long

if (typeof Array.prototype.forEach != 'function') {
  Array.prototype.forEach = function(callback) {
    for (var i = 0; i < this.length; i++) {
      callback.apply(this, [this[i], i, this])
    }
  }
}
/* webim javascript SDK
 * VER 1.7.0
 */

/* webim API definitions
 */
var msgCache = {}
var webim = {
  // namespace object webim

  /* function init
     *   sdk登录
     * params:
     *   loginInfo      - Object, 登录身份相关参数集合，详见下面
     *   {
     *     sdkAppID     - String, 用户标识接入SDK的应用ID，必填
     *     accountType  - int, 账号类型，必填
     *     identifier   - String, 用户帐号,必须是字符串类型，必填
     *     identifierNick   - String, 用户昵称，选填
     *     userSig      - String, 鉴权Token，必须是字符串类型，必填
     *   }
     *   listeners      - Object, 事件回调函数集合, 详见下面
     *   {
     *     onConnNotify - function(connInfo), 用于收到连接状态相关通知的回调函数,目前未使用
     *     jsonpCallback -function(rspData),//IE9(含)以下浏览器用到的jsonp回调函数
     *     onMsgNotify  - function(newMsgList), 用于收到消息通知的回调函数,
     *      newMsgList为新消息数组，格式为[Msg对象]
     *      使用方有两种处理回调: 1)处理newMsgList中的增量消息,2)直接访问webim.MsgStore获取最新的消息
     *     onGroupInfoChangeNotify  - function(groupInfo), 用于监听群组资料变更的回调函数,
     *          groupInfo为新的群组资料信息
     *     onGroupSystemNotifys - Object, 用于监听（多终端同步）群系统消息的回调函数对象
     *
     *   }
     *   options        - Object, 其它选项, 目前未使用
     * return:
     *   (无)
     */
  login: function(loginInfo, listeners, options) {},

  /* function syncMsgs
     *   拉取最新C2C消息
     *   一般不需要使用方直接调用, SDK底层会自动同步最新消息并通知使用方, 一种有用的调用场景是用户手动触发刷新消息
     * params:
     *   cbOk   - function(msgList)类型, 当同步消息成功时的回调函数, msgList为新消息数组，格式为[Msg对象],
     *            如果此参数为null或undefined则同步消息成功后会像自动同步那样回调cbNotify
     *   cbErr  - function(err)类型, 当同步消息失败时的回调函数, err为错误对象
     * return:
     *   (无)
     */
  syncMsgs: function(cbOk, cbErr) {},

  /* function getC2CHistoryMsgs
     * 拉取C2C漫游消息
     * params:
     *   options    - 请求参数
     *   cbOk   - function(msgList)类型, 成功时的回调函数, msgList为消息数组，格式为[Msg对象],
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getC2CHistoryMsgs: function(options, cbOk, cbErr) {},

  /* function syncGroupMsgs
     * 拉取群漫游消息
     * params:
     *   options    - 请求参数
     *   cbOk   - function(msgList)类型, 成功时的回调函数, msgList为消息数组，格式为[Msg对象],
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  syncGroupMsgs: function(options, cbOk, cbErr) {},

  /* function sendMsg
     *   发送一条消息
     * params:
     *   msg    - webim.Msg类型, 要发送的消息对象
     *   cbOk   - function()类型, 当发送消息成功时的回调函数
     *   cbErr  - function(err)类型, 当发送消息失败时的回调函数, err为错误对象
     * return:
     *   (无)
     */
  sendMsg: function(msg, cbOk, cbErr) {},

  /* function logout
     *   sdk登出
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  logout: function(cbOk, cbErr) {},

  /* function setAutoRead
     * 设置会话自动已读上报标志
     * params:
     *   selSess    - webim.Session类型, 当前会话
     *   isOn   - boolean, 将selSess的自动已读消息标志改为isOn，同时是否上报当前会话已读消息
     *   isResetAll - boolean，是否重置所有会话的自动已读标志
     * return:
     *   (无)
     */
  setAutoRead: function(selSess, isOn, isResetAll) {},

  /* function getProfilePortrait
     *   拉取资料（搜索用户）
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getProfilePortrait: function(options, cbOk, cbErr) {},

  /* function setProfilePortrait
     *   设置个人资料
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  setProfilePortrait: function(options, cbOk, cbErr) {},

  /* function applyAddFriend
     *   申请添加好友
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  applyAddFriend: function(options, cbOk, cbErr) {},

  /* function getPendency
     *   拉取好友申请
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getPendency: function(options, cbOk, cbErr) {},

  /* function deletePendency
     *   删除好友申请
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  deletePendency: function(options, cbOk, cbErr) {},

  /* function responseFriend
     *   响应好友申请
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  responseFriend: function(options, cbOk, cbErr) {},

  /* function getAllFriend
     *   拉取我的好友
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getAllFriend: function(options, cbOk, cbErr) {},

  /* function deleteFriend
     *   删除好友
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  deleteFriend: function(options, cbOk, cbErr) {},

  /* function addBlackList
     *   增加黑名单
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  addBlackList: function(options, cbOk, cbErr) {},

  /* function getBlackList
     *   删除黑名单
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getBlackList: function(options, cbOk, cbErr) {},

  /* function deleteBlackList
     *   我的黑名单
     * params:
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  deleteBlackList: function(options, cbOk, cbErr) {},

  /* function uploadPic
     *   上传图片
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  uploadPic: function(options, cbOk, cbErr) {},

  /* function createGroup
     *   创建群
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  createGroup: function(options, cbOk, cbErr) {},

  /* function applyJoinGroup
     *   申请加群
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  applyJoinGroup: function(options, cbOk, cbErr) {},

  /* function handleApplyJoinGroup
     *   处理申请加群(同意或拒绝)
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  handleApplyJoinGroup: function(options, cbOk, cbErr) {},

  /* function deleteApplyJoinGroupPendency
     *   删除加群申请
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  deleteApplyJoinGroupPendency: function(options, cbOk, cbErr) {},

  /* function quitGroup
     *  主动退群
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  quitGroup: function(options, cbOk, cbErr) {},

  /* function getGroupPublicInfo
     *   读取群公开资料-高级接口
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getGroupPublicInfo: function(options, cbOk, cbErr) {},

  /* function getGroupInfo
     *   读取群详细资料-高级接口
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getGroupInfo: function(options, cbOk, cbErr) {},

  /* function modifyGroupBaseInfo
     *   修改群基本资料
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  modifyGroupBaseInfo: function(options, cbOk, cbErr) {},

  /* function destroyGroup
     *  解散群
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  destroyGroup: function(options, cbOk, cbErr) {},

  /* function getJoinedGroupListHigh
     *   获取我的群组-高级接口
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getJoinedGroupListHigh: function(options, cbOk, cbErr) {},

  /* function getGroupMemberInfo
     *   获取群组成员列表
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getGroupMemberInfo: function(options, cbOk, cbErr) {},

  /* function addGroupMember
     *   邀请好友加群
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  addGroupMember: function(options, cbOk, cbErr) {},

  /* function modifyGroupMember
     *   修改群成员资料（角色或者群消息提类型示）
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  modifyGroupMember: function(options, cbOk, cbErr) {},

  /* function forbidSendMsg
     *   设置群成员禁言时间
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  forbidSendMsg: function(options, cbOk, cbErr) {},

  /* function deleteGroupMember
     *   删除群成员
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  deleteGroupMember: function(options, cbOk, cbErr) {},

  /* function getPendencyGroup
     *   获取群组未决列表
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getPendencyGroup: function(options, cbOk, cbErr) {},

  /* function getPendencyReport
     *   好友未决已读上报
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getPendencyReport: function(options, cbOk, cbErr) {},

  /* function getPendencyGroupRead
     *   群组未决已读上报
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  getPendencyGroupRead: function(options, cbOk, cbErr) {},

  /* function sendCustomGroupNotify
     *   发送自定义群通知
     * params:
     *   options    - 请求参数，详见api文档
     *   cbOk   - function()类型, 成功时回调函数
     *   cbErr  - function(err)类型, 失败时回调函数, err为错误对象
     * return:
     *   (无)
     */
  sendCustomGroupNotify: function(options, cbOk, cbErr) {},

  /* class webim.Msg
     *   一条消息的描述类, 消息发送、接收的API中都会涉及此类型的对象
     * properties:
     *   sess   - Session object-ref, 消息所属的会话(e.g:我与好友A的C2C会话，我与群组G的GROUP会话)
     *   isSend - Boolean, true表示是我发出消息, false表示是发给我的消息)
     *   seq    - Integer, 消息序列号, 用于判断消息是否同一条
     *   random - Integer, 消息随机数,用于判断消息是否同一条
     *   time   - Integer, 消息时间戳, 为unix timestamp
     *   fromAccount -String,  消息发送者帐号
     *   subType -Integer,  消息子类型，c2c消息时，0-表示普通消息；群消息时，0-普通消息，1-点赞消息，2-提示消息
     *   fromAccountNick -String,  消息发送者昵称
     *   elems  - Array of webim.Msg.Elem, 描述消息内容的元素列表
     * constructor:
     *   Msg(sess, isSend, seq,random time,fromAccount) - 构造函数, 参数定义同上面properties中定义
     * methods:
     *   addText(text)  - 向elems中添加一个TEXT元素
     *   addFace(face)  - 向elems中添加一个FACE元素
     *   toHtml()       - 转成可展示的html String
     *addFace
     * sub-class webim.Msg.Elem
     *   消息中一个组成元素的描述类, 一条消息的内容被抽象描述为N个元素的有序列表
     * properties:
     *   type   - 元素类型, 目前有TEXT(文本)、FACE(表情)、IMAGE(图片)等
     *   content- 元素内容体, 当TEXT时为String, 当PIC时为UrlString
     * constructor:
     *   Elem(type, content) - 构造函数, 参数定义同上面properties中定义
     *
     * sub-class webim.Msg.Elem.TextElem
     *   文本
     * properties:
     *   text  - String 内容
     * constructor:
     *   TextElem(text) - 构造函数, 参数定义同上面properties中定义
     *
     * sub-class webim.Msg.Elem.FaceElem
     *   表情
     * properties:
     *   index  - Integer 表情索引, 用户自定义
     *   data   - String 额外数据，用户自定义
     * constructor:
     *   FaceElem(index,data) - 构造函数, 参数定义同上面properties中定义
     *
     *
     */
  Msg: function(sess, isSend, seq, random, time, fromAccount, subType, fromAccountNick) {
    /*Class constructor*/
  },

  /* singleton object MsgStore
     * webim.MsgStore是消息数据的Model对象(参考MVC概念), 它提供接口访问当前存储的会话和消息数据
     * 下面说明下会话数据类型: Session
     *
     * class Session
     *   一个Session对象描述一个会话，会话可简单理解为最近会话列表的一个条目，它由两个字段唯一标识:
     *     type - String, 会话类型(如"C2C", "GROUP", ...)
     *     id   - String, 会话ID(如C2C类型中为对方帐号,"C2C"时为好友ID,"GROUP"时为群ID)
     * properties:
     *   (Session对象未对外暴露任何属性字段, 所有访问通过下面的getter方法进行)
     * methods:
     *   type()     - String, 返回会话类型,"C2C"表示与好友私聊，"GROUP"表示群聊
     *   id()       - String, 返回会话ID
     *   name()     - String, 返回会话标题(如C2C类型中为对方的昵称,暂不支持)
     *   icon()     - String, 返回会话图标(对C2C类型中为对方的头像URL，暂不支持)
     *   unread()           - Integer, 返回会话未读条数
     *   time()     - Integer, 返回会话最后活跃时间, 为unix timestamp
     *   curMaxMsgSeq() - Integer, 返回会话最大消息序列号
     *   msgCount() - Integer, 返回会话中所有消息条数
     *   msg(index) - webim.Msg, 返回会话中第index条消息
     */
  MsgStore: {
    /* function sessMap
         *   获取所有会话
         * return:
         *   所有会话对象
         */
    sessMap: function() {
      return {
        /*Object*/
      }
    },
    /* function sessCount
         *   获取当前会话的个数
         * return:
         *   Integer, 会话个数
         */
    sessCount: function() {
      return 0
    },

    /* function sessByTypeId
         *   根据会话类型和会话ID取得相应会话
         * params:
         *   type   - String, 会话类型(如"C2C", "GROUP", ...)
         *   id     - String, 会话ID(如对方ID)
         * return:
         *   Session, 会话对象(说明见上面)
         */
    sessByTypeId: function(type, id) {
      return {
        /*Session Object*/
      }
    },
    /* function delSessByTypeId
         *   根据会话类型和会话ID删除相应会话
         * params:
         *   type   - String, 会话类型(如"C2C", "GROUP", ...)
         *   id     - String, 会话ID(如对方ID)
         * return:
         *   Boolean, 布尔类型
         */
    delSessByTypeId: function(type, id) {
      return true
    },

    /* function resetCookieAndSyncFlag
         *   重置上一次读取新c2c消息Cookie和是否继续拉取标记
         * return:
         *
         */
    resetCookieAndSyncFlag: function() {},

    downloadMap: {}
  }
}

/* webim API implementation
 */
;(function(webim) {
  //sdk版本
  var SDK = {
    VERSION: '1.7.0', //sdk版本号
    APPID: '537048168', //web im sdk 版本 APPID
    PLAATFORM: '10' //发送请求时判断其是来自web端的请求
  }

  //是否启用正式环境，默认启用
  var isAccessFormaEnvironment = true
  // var isAccessFormaEnvironment = false;

  //后台接口主机
  var SRV_HOST = {
    FORMAL: {
      COMMON: 'https://webim.tim.qq.com',
      PIC: 'https://pic.tim.qq.com'
    },
    TEST: {
      COMMON: 'https://test.tim.qq.com',
      PIC: 'https://pic.tim.qq.com'
    }
  }

  //浏览器版本信息
  var BROWSER_INFO = {}
  //是否为ie9（含）以下
  var lowerBR = false

  //服务名称
  var SRV_NAME = {
    OPEN_IM: 'openim', //私聊（拉取未读c2c消息，长轮询，c2c消息已读上报等）服务名
    GROUP: 'group_open_http_svc', //群组管理（拉取群消息，创建群，群成员管理，群消息已读上报等）服务名
    FRIEND: 'sns', //关系链管理（好友管理，黑名单管理等）服务名
    PROFILE: 'profile', //资料管理（查询，设置个人资料等）服务名
    RECENT_CONTACT: 'recentcontact', //最近联系人服务名
    PIC: 'openpic', //图片（或文件）服务名
    BIG_GROUP: 'group_open_http_noauth_svc', //直播大群 群组管理（申请加大群）服务名
    BIG_GROUP_LONG_POLLING: 'group_open_long_polling_http_noauth_svc', //直播大群 长轮询（拉取消息等）服务名
    IM_OPEN_STAT: 'imopenstat', //质量上报，统计接口错误率
    DEL_CHAT: 'recentcontact' //删除会话
  }

  //不同服务对应的版本号
  var SRV_NAME_VER = {
    openim: 'v4',
    group_open_http_svc: 'v4',
    sns: 'v4',
    profile: 'v4',
    recentcontact: 'v4',
    openpic: 'v4',
    group_open_http_noauth_svc: 'v1',
    group_open_long_polling_http_noauth_svc: 'v1',
    imopenstat: 'v4'
  }

  //不同的命令名对应的上报类型ID，用于接口质量上报
  var CMD_EVENT_ID_MAP = {
    login: 1, //登录
    pic_up: 3, //上传图片
    apply_join_group: 9, //申请加入群组
    create_group: 10, //创建群组
    longpolling: 18, //普通长轮询
    send_group_msg: 19, //群聊
    sendmsg: 20 //私聊
  }

  //聊天类型
  var SESSION_TYPE = {
    C2C: 'C2C', //私聊
    GROUP: 'GROUP' //群聊
  }

  //最近联系人类型
  var RECENT_CONTACT_TYPE = {
    C2C: 1, //好友
    GROUP: 2 //群
  }

  //消息最大长度（字节）
  var MSG_MAX_LENGTH = {
    C2C: 12000, //私聊消息
    GROUP: 8898 //群聊
  }

  //后台接口返回类型
  var ACTION_STATUS = {
    OK: 'OK', //成功
    FAIL: 'FAIL' //失败
  }

  var ERROR_CODE_CUSTOM = 99999 //自定义后台接口返回错误码

  //消息元素类型
  var MSG_ELEMENT_TYPE = {
    TEXT: 'TIMTextElem', //文本
    FACE: 'TIMFaceElem', //表情
    IMAGE: 'TIMImageElem', //图片
    CUSTOM: 'TIMCustomElem', //自定义
    SOUND: 'TIMSoundElem', //语音,只支持显示
    FILE: 'TIMFileElem', //文件,只支持显示
    LOCATION: 'TIMLocationElem', //地理位置
    GROUP_TIP: 'TIMGroupTipElem' //群提示消息,只支持显示
  }

  //图片类型
  var IMAGE_TYPE = {
    ORIGIN: 1, //原图
    LARGE: 2, //缩略大图
    SMALL: 3 //缩略小图
  }
  //图片格式
  var IMAGE_FORMAT = {
    JPG: 0x1,
    JPEG: 0x1,
    GIF: 0x2,
    PNG: 0x3,
    BMP: 0x4,
    UNKNOWN: 0xff
  }

  //上传资源包类型
  var UPLOAD_RES_PKG_FLAG = {
    RAW_DATA: 0, //原始数据
    BASE64_DATA: 1 //base64编码数据
  }

  //下载文件配置
  var DOWNLOAD_FILE = {
    BUSSINESS_ID: '10001', //下载文件业务ID
    AUTH_KEY: '617574686b6579', //下载文件authkey
    SERVER_IP: '182.140.186.147' //下载文件服务器IP
  }

  //下载文件类型
  var DOWNLOAD_FILE_TYPE = {
    SOUND: 2106, //语音
    FILE: 2107 //普通文件
  }

  //上传资源类型
  var UPLOAD_RES_TYPE = {
    IMAGE: 1, //图片
    FILE: 2, //文件
    SHORT_VIDEO: 3, //短视频
    SOUND: 4 //语音，PTT
  }

  //版本号，用于上传图片或文件接口
  var VERSION_INFO = {
    APP_VERSION: '2.1', //应用版本号
    SERVER_VERSION: 1 //服务端版本号
  }

  //长轮询消息类型
  var LONG_POLLINNG_EVENT_TYPE = {
    C2C: 1, //新的c2c消息通知
    GROUP_COMMON: 3, //新的群普通消息
    GROUP_TIP: 4, //新的群提示消息
    GROUP_SYSTEM: 5, //新的群系统消息
    GROUP_TIP2: 6, //新的群提示消息2
    FRIEND_NOTICE: 7, //好友系统通知
    PROFILE_NOTICE: 8, //资料系统通知
    C2C_COMMON: 9, //新的C2C消息
    C2C_EVENT: 10
  }

  //c2c消息子类型
  var C2C_MSG_SUB_TYPE = {
    COMMON: 0 //普通消息
  }
  //c2c消息子类型
  var C2C_EVENT_SUB_TYPE = {
    READED: 92, //已读消息同步
    KICKEDOUT: 96
  }

  //群消息子类型
  var GROUP_MSG_SUB_TYPE = {
    COMMON: 0, //普通消息
    LOVEMSG: 1, //点赞消息
    TIP: 2, //提示消息
    REDPACKET: 3 //红包消息
  }

  //群消息优先级类型
  var GROUP_MSG_PRIORITY_TYPE = {
    REDPACKET: 1, //红包消息
    COMMON: 2, //普通消息
    LOVEMSG: 3 //点赞消息
  }

  //群提示消息类型
  var GROUP_TIP_TYPE = {
    JOIN: 1, //加入群组
    QUIT: 2, //退出群组
    KICK: 3, //被踢出群组
    SET_ADMIN: 4, //被设置为管理员
    CANCEL_ADMIN: 5, //被取消管理员
    MODIFY_GROUP_INFO: 6, //修改群资料
    MODIFY_MEMBER_INFO: 7 //修改群成员信息
  }

  //群提示消息-群资料变更类型
  var GROUP_TIP_MODIFY_GROUP_INFO_TYPE = {
    FACE_URL: 1, //修改群头像URL
    NAME: 2, //修改群名称
    OWNER: 3, //修改群主
    NOTIFICATION: 4, //修改群公告
    INTRODUCTION: 5 //修改群简介
  }

  //群系统消息类型
  var GROUP_SYSTEM_TYPE = {
    JOIN_GROUP_REQUEST: 1, //申请加群请求（只有管理员会收到）
    JOIN_GROUP_ACCEPT: 2, //申请加群被同意（只有申请人能够收到）
    JOIN_GROUP_REFUSE: 3, //申请加群被拒绝（只有申请人能够收到）
    KICK: 4, //被管理员踢出群(只有被踢者接收到)
    DESTORY: 5, //群被解散(全员接收)
    CREATE: 6, //创建群(创建者接收, 不展示)
    INVITED_JOIN_GROUP_REQUEST: 7, //邀请加群(被邀请者接收)
    QUIT: 8, //主动退群(主动退出者接收, 不展示)
    SET_ADMIN: 9, //设置管理员(被设置者接收)
    CANCEL_ADMIN: 10, //取消管理员(被取消者接收)
    REVOKE: 11, //群已被回收(全员接收, 不展示)
    READED: 15, //群消息已读同步
    CUSTOM: 255, //用户自定义通知(默认全员接收)
    INVITED_JOIN_GROUP_REQUEST_AGREE: 12 //邀请加群(被邀请者需同意)
  }

  //好友系统通知子类型
  var FRIEND_NOTICE_TYPE = {
    FRIEND_ADD: 1, //好友表增加
    FRIEND_DELETE: 2, //好友表删除
    PENDENCY_ADD: 3, //未决增加
    PENDENCY_DELETE: 4, //未决删除
    BLACK_LIST_ADD: 5, //黑名单增加
    BLACK_LIST_DELETE: 6, //黑名单删除
    PENDENCY_REPORT: 7, //未决已读上报
    FRIEND_UPDATE: 8 //好友数据更新
  }

  //资料系统通知子类型
  var PROFILE_NOTICE_TYPE = {
    PROFILE_MODIFY: 1 //资料修改
  }

  //腾讯登录服务错误码（用于托管模式）
  var TLS_ERROR_CODE = {
    OK: 0, //成功
    SIGNATURE_EXPIRATION: 11 //用户身份凭证过期
  }

  //长轮询连接状态
  var CONNECTION_STATUS = {
    INIT: -1, //初始化
    ON: 0, //连接正常
    RECONNECT: 1, //连接恢复正常
    OFF: 9999 //连接已断开,可能是用户网络问题，或者长轮询接口报错引起的
  }

  var UPLOAD_PIC_BUSSINESS_TYPE = {
    //图片业务类型
    GROUP_MSG: 1, //私聊图片
    C2C_MSG: 2, //群聊图片
    USER_HEAD: 3, //用户头像
    GROUP_HEAD: 4 //群头像
  }

  var FRIEND_WRITE_MSG_ACTION = {
    //好友输入消息状态
    ING: 14, //正在输入
    STOP: 15 //停止输入
  }

  //ajax默认超时时间，单位：毫秒
  var ajaxDefaultTimeOut = 15000

  //大群长轮询接口返回正常时，延时一定时间再发起下一次请求
  var OK_DELAY_TIME = 1000

  //大群长轮询接口发生错误时，延时一定时间再发起下一次请求
  var ERROR_DELAY_TIME = 5000

  //群提示消息最多显示人数
  var GROUP_TIP_MAX_USER_COUNT = 10

  //长轮询连接状态
  var curLongPollingStatus = CONNECTION_STATUS.INIT

  //当长轮询连接断开后，是否已经回调过
  var longPollingOffCallbackFlag = false

  //当前长轮询返回错误次数
  var curLongPollingRetErrorCount = 0

  //长轮询默认超时时间，单位：毫秒
  var longPollingDefaultTimeOut = 60000

  //长轮询返回错误次数达到一定值后，发起新的长轮询请求间隔时间，单位：毫秒
  var longPollingIntervalTime = 5000

  //没有新消息时，长轮询返回60008错误码是正常的
  var longPollingTimeOutErrorCode = 60008

  //多实例登录被kick的错误码
  var longPollingKickedErrorCode = 91101

  var LongPollingId = null

  //当前大群长轮询返回错误次数
  var curBigGroupLongPollingRetErrorCount = 0

  //最大允许长轮询返回错误次数
  var LONG_POLLING_MAX_RET_ERROR_COUNT = 10

  //上传重试累计
  var Upload_Retry_Times = 0
  //最大上传重试
  var Upload_Retry_Max_Times = 20

  //ie7/8/9采用jsonp方法解决ajax跨域限制
  var jsonpRequestId = 0 //jsonp请求id
  //最新jsonp请求返回的json数据
  var jsonpLastRspData = null
  //兼容ie7/8/9,jsonp回调函数
  var jsonpCallback = null

  var uploadResultIframeId = 0 //用于上传图片的iframe id

  var ipList = [] //文件下载地址
  var authkey = null //文件下载票据
  var expireTime = null //文件下载票据超时时间

  //错误码
  var ERROR = {}
  //当前登录用户
  var ctx = {
    sdkAppID: null,
    appIDAt3rd: null,
    accountType: null,
    identifier: null,
    tinyid: null,
    identifierNick: null,
    userSig: null,
    a2: null,
    contentType: 'json',
    apn: 1
  }
  var opt = {}
  var xmlHttpObjSeq = 0 //ajax请求id
  var xmlHttpObjMap = {} //发起的ajax请求
  var curSeq = 0 //消息seq
  var tempC2CMsgList = [] //新c2c消息临时缓存
  var tempC2CHistoryMsgList = [] //漫游c2c消息临时缓存

  var maxApiReportItemCount = 20 //一次最多上报条数
  var apiReportItems = [] //暂存api接口质量上报数据

  var Resources = {
    downloadMap: {}
  }

  //表情标识字符和索引映射关系对象，用户可以自定义
  var emotionDataIndexs = {
    '[惊讶]': 0,
    '[撇嘴]': 1,
    '[色]': 2,
    '[发呆]': 3,
    '[得意]': 4,
    '[流泪]': 5,
    '[害羞]': 6,
    '[闭嘴]': 7,
    '[睡]': 8,
    '[大哭]': 9,
    '[尴尬]': 10,
    '[发怒]': 11,
    '[调皮]': 12,
    '[龇牙]': 13,
    '[微笑]': 14,
    '[难过]': 15,
    '[酷]': 16,
    '[冷汗]': 17,
    '[抓狂]': 18,
    '[吐]': 19,
    '[偷笑]': 20,
    '[可爱]': 21,
    '[白眼]': 22,
    '[傲慢]': 23,
    '[饿]': 24,
    '[困]': 25,
    '[惊恐]': 26,
    '[流汗]': 27,
    '[憨笑]': 28,
    '[大兵]': 29,
    '[奋斗]': 30,
    '[咒骂]': 31,
    '[疑问]': 32,
    '[嘘]': 33,
    '[晕]': 34
  }

  //表情对象，用户可以自定义
  var emotions = {
    '0': [
      '[惊讶]',
      ''
    ],
    '1': [
      '[撇嘴]',
      ''
    ],
    '2': [
      '[色]',
      ''
    ],
    '3': [
      '[发呆]',
      ''
    ],
    '4': [
      '[得意]',
      ''
    ],
    '5': [
      '[流泪]',
      ''
    ],
    '6': [
      '[害羞]',
      ''
    ],
    '7': [
      '[闭嘴]',
      ''
    ],
    '8': [
      '[睡]',
      ''
    ],
    '9': [
      '[大哭]',
      ''
    ],
    '10': [
      '[尴尬]',
      ''
    ],
    '11': [
      '[发怒]',
      ''
    ],
    '12': [
      '[调皮]',
      ''
    ],
    '13': [
      '[龇牙]',
      ''
    ],
    '14': [
      '[微笑]',
      ''
    ],
    '15': [
      '[难过]',
      ''
    ],
    '16': [
      '[酷]',
      ''
    ],
    '17': [
      '[冷汗]',
      ''
    ],
    '18': [
      '[抓狂]',
      ''
    ],
    '19': [
      '[吐]',
      ''
    ],
    '20': [
      '[偷笑]',
      ''
    ],
    '21': [
      '[可爱]',
      ''
    ],
    '22': [
      '[白眼]',
      ''
    ],
    '23': [
      '[傲慢]',
      ''
    ],
    '24': [
      '[饿]',
      ''
    ],
    '25': [
      '[困]',
      ''
    ],
    '26': [
      '[惊恐]',
      ''
    ],
    '27': [
      '[流汗]',
      ''
    ],
    '28': [
      '[憨笑]',
      ''
    ],
    '29': [
      '[大兵]',
      ''
    ],
    '30': [
      '[奋斗]',
      ''
    ],
    '31': [
      '[咒骂]',
      ''
    ],
    '32': [
      '[疑问]',
      ''
    ],
    '33': [
      '[嘘]',
      ''
    ],
    '34': [
      '[晕]',
      ''
    ]
  }
  //工具类
  var tool = new function() {
    //格式化时间戳
    //format格式如下：
    //yyyy-MM-dd hh:mm:ss 年月日时分秒(默认格式)
    //yyyy-MM-dd 年月日
    //hh:mm:ss 时分秒
    this.formatTimeStamp = function(timestamp, format) {
      if (!timestamp) {
        return 0
      }
      var formatTime
      format = format || 'yyyy-MM-dd hh:mm:ss'
      var date = new Date(timestamp * 1000)
      var o = {
        'M+': date.getMonth() + 1, //月份
        'd+': date.getDate(), //日
        'h+': date.getHours(), //小时
        'm+': date.getMinutes(), //分
        's+': date.getSeconds() //秒
      }
      if (/(y+)/.test(format)) {
        formatTime = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
      } else {
        formatTime = format
      }
      for (var k in o) {
        if (new RegExp('(' + k + ')').test(formatTime))
          formatTime = formatTime.replace(
            RegExp.$1,
            RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
          )
      }
      return formatTime
    }

    //根据群类型英文名转换成中文名
    this.groupTypeEn2Ch = function(type_en) {
      var type_ch = null
      switch (type_en) {
        case 'Public':
          type_ch = '公开群'
          break
        case 'ChatRoom':
          type_ch = '聊天室'
          break
        case 'Private':
          type_ch = '私有群' //即讨论组
          break
        case 'AVChatRoom':
          type_ch = '直播聊天室'
          break
        default:
          type_ch = type_en
          break
      }
      return type_ch
    }
    //根据群类型中文名转换成英文名
    this.groupTypeCh2En = function(type_ch) {
      var type_en = null
      switch (type_ch) {
        case '公开群':
          type_en = 'Public'
          break
        case '聊天室':
          type_en = 'ChatRoom'
          break
        case '私有群': //即讨论组
          type_en = 'Private'
          break
        case '直播聊天室':
          type_en = 'AVChatRoom'
          break
        default:
          type_en = type_ch
          break
      }
      return type_en
    }
    //根据群身份英文名转换成群身份中文名
    this.groupRoleEn2Ch = function(role_en) {
      var role_ch = null
      switch (role_en) {
        case 'Member':
          role_ch = '成员'
          break
        case 'Admin':
          role_ch = '管理员'
          break
        case 'Owner':
          role_ch = '群主'
          break
        default:
          role_ch = role_en
          break
      }
      return role_ch
    }
    //根据群身份中文名转换成群身份英文名
    this.groupRoleCh2En = function(role_ch) {
      var role_en = null
      switch (role_ch) {
        case '成员':
          role_en = 'Member'
          break
        case '管理员':
          role_en = 'Admin'
          break
        case '群主':
          role_en = 'Owner'
          break
        default:
          role_en = role_ch
          break
      }
      return role_en
    }
    //根据群消息提示类型英文转换中文
    this.groupMsgFlagEn2Ch = function(msg_flag_en) {
      var msg_flag_ch = null
      switch (msg_flag_en) {
        case 'AcceptAndNotify':
          msg_flag_ch = '接收并提示'
          break
        case 'AcceptNotNotify':
          msg_flag_ch = '接收不提示'
          break
        case 'Discard':
          msg_flag_ch = '屏蔽'
          break
        default:
          msg_flag_ch = msg_flag_en
          break
      }
      return msg_flag_ch
    }
    //根据群消息提示类型中文名转换英文名
    this.groupMsgFlagCh2En = function(msg_flag_ch) {
      var msg_flag_en = null
      switch (msg_flag_ch) {
        case '接收并提示':
          msg_flag_en = 'AcceptAndNotify'
          break
        case '接收不提示':
          msg_flag_en = 'AcceptNotNotify'
          break
        case '屏蔽':
          msg_flag_en = 'Discard'
          break
        default:
          msg_flag_en = msg_flag_ch
          break
      }
      return msg_flag_en
    }
    //将空格和换行符转换成HTML标签
    this.formatText2Html = function(text) {
      var html = text
      if (html) {
        html = this.xssFilter(html) //用户昵称或群名称等字段会出现脚本字符串
        html = html.replace(/ /g, '&nbsp;')
        html = html.replace(/\n/g, '<br/>')
      }
      return html
    }
    //将HTML标签转换成空格和换行符
    this.formatHtml2Text = function(html) {
      var text = html
      if (text) {
        text = text.replace(/&nbsp;/g, ' ')
        text = text.replace(/<br\/>/g, '\n')
      }
      return text
    }
    //获取字符串(UTF-8编码)所占字节数
    //参考：http://zh.wikipedia.org/zh-cn/UTF-8
    this.getStrBytes = function(str) {
      if (str == null || str === undefined) return 0
      if (typeof str != 'string') {
        return 0
      }
      var total = 0,
        charCode,
        i,
        len
      for (i = 0, len = str.length; i < len; i++) {
        charCode = str.charCodeAt(i)
        if (charCode <= 0x007f) {
          total += 1 //字符代码在000000 – 00007F之间的，用一个字节编码
        } else if (charCode <= 0x07ff) {
          total += 2 //000080 – 0007FF之间的字符用两个字节
        } else if (charCode <= 0xffff) {
          total += 3 //000800 – 00D7FF 和 00E000 – 00FFFF之间的用三个字节，注: Unicode在范围 D800-DFFF 中不存在任何字符
        } else {
          total += 4 //010000 – 10FFFF之间的用4个字节
        }
      }
      return total
    }

    //防止XSS攻击
    this.xssFilter = function(val) {
      val = val.toString()
      val = val.replace(/[<]/g, '&lt;')
      val = val.replace(/[>]/g, '&gt;')
      val = val.replace(/"/g, '&quot;')
      //val = val.replace(/'/g, "&#039;");
      return val
    }

    //去掉头尾空白符
    this.trimStr = function(str) {
      if (!str) return ''
      str = str.toString()
      return str.replace(/(^\s*)|(\s*$)/g, '')
    }
    //判断是否为8位整数
    this.validNumber = function(str) {
      str = str.toString()
      return str.match(/(^\d{1,8}$)/g)
    }
    this.getReturnError = function(errorInfo, errorCode) {
      if (!errorCode) {
        errorCode = -100
      }
      var error = {
        ActionStatus: ACTION_STATUS.FAIL,
        ErrorCode: errorCode,
        ErrorInfo: errorInfo + '[' + errorCode + ']'
      }
      return error
    }
    //设置cookie
    //name 名字
    //value 值
    //expires 有效期(单位：秒)
    //path
    //domain 作用域
    this.setCookie = function(name, value, expires, path, domain) {
      var exp = new Date()
      exp.setTime(exp.getTime() + expires * 1000)
      document.cookie = name + '=' + escape(value) + ';expires=' + exp.toGMTString()
    }
    //获取cookie
    this.getCookie = function(name) {
      var result = document.cookie.match(new RegExp('(^| )' + name + '=([^;]*)(;|$)'))
      if (result != null) {
        return unescape(result[2])
      }
      return null
    }
    //删除cookie
    this.delCookie = function(name) {
      var exp = new Date()
      exp.setTime(exp.getTime() - 1)
      var value = this.getCookie(name)
      if (value != null) document.cookie = name + '=' + escape(value) + ';expires=' + exp.toGMTString()
    }
    //根据名字获取url参数值
    this.getQueryString = function(name) {
      var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i')
      var r = window.location.search.substr(1).match(reg)
      if (r != null) return unescape(r[2])
      return null
    }
    //判断IE版本号，ver表示版本号
    this.isIE = function(ver) {
      var b = document.createElement('b')
      b.innerHTML = '<!--[if IE ' + ver + ']><i></i><![endif]-->'
      return b.getElementsByTagName('i').length === 1
    }
    //判断浏览器版本
    this.getBrowserInfo = function() {
      var Sys = {}
      var ua = navigator.userAgent.toLowerCase()
      log.info('navigator.userAgent=' + ua)
      var s
      ;(s = ua.match(/msie ([\d.]+)/))
        ? (Sys.ie = s[1])
        : (s = ua.match(/firefox\/([\d.]+)/))
          ? (Sys.firefox = s[1])
          : (s = ua.match(/chrome\/([\d.]+)/))
            ? (Sys.chrome = s[1])
            : (s = ua.match(/opera.([\d.]+)/))
              ? (Sys.opera = s[1])
              : (s = ua.match(/version\/([\d.]+).*safari/))
                ? (Sys.safari = s[1])
                : 0
      if (Sys.ie) {
        //Js判断为IE浏览器
        //ie10的判断这里有个问题
        // Mozilla/5.0 (compatible; MSIE 9.0; qdesk 2.5.1277.202; Windows NT 6.1; WOW64; Trident/6.0)
        // 是IE10 而不是IE9
        if (ua.match(/trident\/(\d)\./) && ua.match(/trident\/(\d)\./)[1] == 6) {
          Sys.ie = 10
        }
        return {
          type: 'ie',
          ver: Sys.ie
        }
      }
      if (Sys.firefox) {
        //Js判断为火狐(firefox)浏览器
        return {
          type: 'firefox',
          ver: Sys.firefox
        }
      }
      if (Sys.chrome) {
        //Js判断为谷歌chrome浏览器
        return {
          type: 'chrome',
          ver: Sys.chrome
        }
      }
      if (Sys.opera) {
        //Js判断为opera浏览器
        return {
          type: 'opera',
          ver: Sys.opera
        }
      }
      if (Sys.safari) {
        //Js判断为苹果safari浏览器
        return {
          type: 'safari',
          ver: Sys.safari
        }
      }
      return {
        type: 'unknow',
        ver: -1
      }
    }

    this.replaceObject = function(keyMap, json) {
      for (var a in json) {
        if (keyMap[a]) {
          json[keyMap[a]] = json[a]
          delete json[a]
          if (json[keyMap[a]] instanceof Array) {
            var len = json[keyMap[a]].length
            for (var i = 0; i < len; i++) {
              json[keyMap[a]][i] = this.replaceObject(keyMap, json[keyMap[a]][i])
            }
          } else if (typeof json[keyMap[a]] === 'object') {
            json[keyMap[a]] = this.replaceObject(keyMap, json[keyMap[a]])
          }
        }
      }
      return json
    }
  }()

  //日志对象
  var log = new function() {
    var on = true

    this.setOn = function(onFlag) {
      on = onFlag
    }

    this.getOn = function() {
      return on
    }

    this.error = function(logStr) {
      try {
        on && console.error(logStr)
      } catch (e) {}
    }
    this.warn = function(logStr) {
      try {
        on && console.warn(logStr)
      } catch (e) {}
    }
    this.info = function(logStr) {
      try {
        on && console.info(logStr)
      } catch (e) {}
    }
    this.debug = function(logStr) {
      try {
        on && console.debug(logStr)
      } catch (e) {}
    }
  }()
  //获取unix时间戳
  var unixtime = function(d) {
    if (!d) d = new Date()
    return Math.round(d.getTime() / 1000)
  }
  //时间戳转日期
  var fromunixtime = function(t) {
    return new Date(t * 1000)
  }
  //获取下一个消息序号
  var nextSeq = function() {
    if (curSeq) {
      curSeq = curSeq + 1
    } else {
      curSeq = Math.round(Math.random() * 10000000)
    }
    return curSeq
  }
  //产生随机数
  var createRandom = function() {
    return Math.round(Math.random() * 4294967296)
  }

  //获取ajax请求对象
  var getXmlHttp = function() {
    var xmlhttp = null
    if (window.XMLHttpRequest) {
      xmlhttp = new XMLHttpRequest()
    } else {
      try {
        xmlhttp = new ActiveXObject('Msxml2.XMLHTTP')
      } catch (e) {
        try {
          xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
        } catch (e) {
          return null
        }
      }
    }
    return xmlhttp
  }
  //发起ajax请求
  var ajaxRequest = function(meth, url, req, timeout, content_type, isLongPolling, cbOk, cbErr) {
    var xmlHttpObj = getXmlHttp()

    var error, errInfo
    if (!xmlHttpObj) {
      errInfo = '创建请求失败'
      var error = tool.getReturnError(errInfo, -1)
      log.error(errInfo)
      if (cbErr) cbErr(error)
      return
    }
    //保存ajax请求对象
    xmlHttpObjSeq++
    xmlHttpObjMap[xmlHttpObjSeq] = xmlHttpObj

    xmlHttpObj.open(meth, url, true)
    xmlHttpObj.onreadystatechange = function() {
      if (xmlHttpObj.readyState == 4) {
        xmlHttpObjMap[xmlHttpObjSeq] = null //清空
        if (xmlHttpObj.status == 200) {
          if (cbOk) cbOk(xmlHttpObj.responseText)
          xmlHttpObj = null
          curLongPollingRetErrorCount = curBigGroupLongPollingRetErrorCount = 0
        } else {
          xmlHttpObj = null
          //避免刷新的时候，由于abord ajax引起的错误回调
          setTimeout(function() {
            var errInfo = '请求服务器失败,请检查你的网络是否正常'
            var error = tool.getReturnError(errInfo, -2)
            //if (!isLongPolling && cbErr) cbErr(error);
            if (cbErr) cbErr(error)
          }, 16)
        }
      }
    }
    xmlHttpObj.setRequestHeader('Content-Type', content_type)
    //设置超时时间
    if (!timeout) {
      timeout = ajaxDefaultTimeOut //设置ajax默认超时时间
    }
    if (timeout) {
      xmlHttpObj.timeout = timeout
      xmlHttpObj.ontimeout = function(event) {
        xmlHttpObj = null
        //var errInfo = "请求服务器超时";
        //var error = tool.getReturnError(errInfo, -3);
        //if (cbErr) cbErr(error);
      }
    }
    //
    if (xmlHttpObj.overrideMimeType) {
      xmlHttpObj.overrideMimeType('application/json;charset=utf-8')
    }

    xmlHttpObj.send(req)
  }
  //发起ajax请求（json格式数据）
  var ajaxRequestJson = function(meth, url, req, timeout, content_type, isLongPolling, cbOk, cbErr) {
    ajaxRequest(
      meth,
      url,
      JSON.stringify(req),
      timeout,
      content_type,
      isLongPolling,
      function(resp) {
        var json = null
        if (resp) json = JSON.parse(resp) //将返回的json字符串转换成json对象
        if (cbOk) cbOk(json)
      },
      cbErr
    )
  }
  //判断用户是否已登录
  var isLogin = function() {
    return ctx.sdkAppID && ctx.identifier
  }
  //检查是否登录
  var checkLogin = function(cbErr, isNeedCallBack) {
    if (!isLogin()) {
      if (isNeedCallBack) {
        var errInfo = '请登录'
        var error = tool.getReturnError(errInfo, -4)

        if (cbErr) cbErr(error)
      }
      return false
    }
    return true
  }

  //检查是否访问正式环境
  var isAccessFormalEnv = function() {
    return isAccessFormaEnvironment
  }

  //根据不同的服务名和命令，获取对应的接口地址
  var getApiUrl = function(srvName, cmd, cbOk, cbErr) {
    var srvHost = SRV_HOST
    if (isAccessFormalEnv()) {
      srvHost = SRV_HOST.FORMAL.COMMON
    } else {
      srvHost = SRV_HOST.TEST.COMMON
    }

    //if (srvName == SRV_NAME.RECENT_CONTACT) {
    //    srvHost = SRV_HOST.TEST.COMMON;
    //}

    if (srvName == SRV_NAME.PIC) {
      if (isAccessFormalEnv()) {
        srvHost = SRV_HOST.FORMAL.PIC
      } else {
        srvHost = SRV_HOST.TEST.PIC
      }
    }

    var url =
      srvHost +
      '/' +
      SRV_NAME_VER[srvName] +
      '/' +
      srvName +
      '/' +
      cmd +
      '?websdkappid=' +
      SDK.APPID +
      '&v=' +
      SDK.VERSION +
      '&platform=' +
      SDK.PLAATFORM

    if (isLogin()) {
      if (cmd == 'login') {
        url += '&identifier=' + encodeURIComponent(ctx.identifier) + '&usersig=' + ctx.userSig
      } else {
        if (ctx.tinyid && ctx.a2) {
          url += '&tinyid=' + ctx.tinyid + '&a2=' + ctx.a2
        } else {
          if (cbErr) {
            log.error('tinyid或a2为空[' + srvName + '][' + cmd + ']')
            cbErr(tool.getReturnError('tinyid或a2为空[' + srvName + '][' + cmd + ']', -5))
            return false
          }
        }
      }
      url += '&contenttype=' + ctx.contentType
    }
    url +=
      '&sdkappid=' + ctx.sdkAppID + '&accounttype=' + ctx.accountType + '&apn=' + ctx.apn + '&reqtime=' + unixtime()
    return url
  }

  //获取语音下载url
  var getSoundDownUrl = function(uuid, senderId) {
    var soundUrl = null
    if (authkey && ipList[0]) {
      soundUrl =
        'http://' +
        ipList[0] +
        '/asn.com/stddownload_common_file?authkey=' +
        authkey +
        '&bid=' +
        DOWNLOAD_FILE.BUSSINESS_ID +
        '&subbid=' +
        ctx.sdkAppID +
        '&fileid=' +
        uuid +
        '&filetype=' +
        DOWNLOAD_FILE_TYPE.SOUND +
        '&openid=' +
        senderId +
        '&ver=0'
    } else {
      log.error('拼接语音下载url不报错：ip或者authkey为空')
    }
    return soundUrl
  }

  //获取文件下载地址
  var getFileDownUrl = function(uuid, senderId, fileName) {
    var fileUrl = null
    if (authkey && ipList[0]) {
      fileUrl =
        'http://' +
        ipList[0] +
        '/asn.com/stddownload_common_file?authkey=' +
        authkey +
        '&bid=' +
        DOWNLOAD_FILE.BUSSINESS_ID +
        '&subbid=' +
        ctx.sdkAppID +
        '&fileid=' +
        uuid +
        '&filetype=' +
        DOWNLOAD_FILE_TYPE.FILE +
        '&openid=' +
        senderId +
        '&ver=0&filename=' +
        encodeURIComponent(fileName)
    } else {
      log.error('拼接文件下载url不报错：ip或者authkey为空')
    }
    Resources.downloadMap['uuid_' + uuid] = fileUrl
    return fileUrl
  }

  //获取文件下载地址
  var getFileDownUrlV2 = function(uuid, senderId, fileName, downFlag, receiverId, busiId, type) {
    var options = {
      From_Account: senderId, //"identifer_0",       // 类型: String, 发送者tinyid
      To_Account: receiverId, //"identifer_1",         // 类型: String, 接收者tinyid
      os_platform: 10, // 类型: Number, 终端的类型 1(android) 2(ios) 3(windows) 10(others...)
      Timestamp: unixtime().toString(), // 类型: Number, 时间戳
      Random: createRandom().toString(), // 类型: Number, 随机值
      request_info: [
        // 类型: Array
        {
          busi_id: busiId, // 类型: Number, 群(1) C2C(2) 其他请联系sdk开发者分配
          download_flag: downFlag, // 类型: Number, 申请下载地址标识  0(申请架平下载地址)  1(申请COS平台下载地址)  2(不需要申请, 直接拿url下载(这里应该不会为2))
          type: type, // 类型: Number, 0(短视频缩略图), 1(文件), 2(短视频), 3(ptt), 其他待分配
          uuid: uuid, // 类型: Number, 唯一标识一个文件的uuid
          version: VERSION_INFO.SERVER_VERSION, // 类型: Number, 架平server版本
          auth_key: authkey, // 类型: String, 认证签名
          ip: ipList[0] // 类型: Number, 架平IP
        }
      ]
    }
    //获取下载地址
    proto_applyDownload(
      options,
      function(resp) {
        if (resp.error_code == 0 && resp.response_info) {
          Resources.downloadMap['uuid_' + options.uuid] = resp.response_info.url
        }
        if (onAppliedDownloadUrl) {
          onAppliedDownloadUrl({
            uuid: options.uuid,
            url: resp.response_info.url,
            maps: Resources.downloadMap
          })
        }
      },
      function(resp) {
        log.error('获取下载地址失败', options.uuid)
      }
    )
  }

  //重置ajax请求
  var clearXmlHttpObjMap = function() {
    //遍历xmlHttpObjMap{}
    for (var seq in xmlHttpObjMap) {
      var xmlHttpObj = xmlHttpObjMap[seq]
      if (xmlHttpObj) {
        xmlHttpObj.abort() //中断ajax请求(长轮询)
        xmlHttpObjMap[xmlHttpObjSeq] = null //清空
      }
    }
    xmlHttpObjSeq = 0
    xmlHttpObjMap = {}
  }

  //重置sdk全局变量
  var clearSdk = function() {
    clearXmlHttpObjMap()

    //当前登录用户
    ctx = {
      sdkAppID: null,
      appIDAt3rd: null,
      accountType: null,
      identifier: null,
      identifierNick: null,
      userSig: null,
      contentType: 'json',
      apn: 1
    }
    opt = {}

    curSeq = 0

    //ie8,9采用jsonp方法解决ajax跨域限制
    jsonpRequestId = 0 //jsonp请求id
    //最新jsonp请求返回的json数据
    jsonpLastRspData = null

    apiReportItems = []

    MsgManager.clear()
    MsgStore.clear()

    //重置longpollingId
    LongPollingId = null
  }

  //登录
  var _login = function(loginInfo, listeners, options, cbOk, cbErr) {
    clearSdk()

    if (options) opt = options
    if (opt.isAccessFormalEnv == false) {
      log.error('请切换为正式环境！！！！')
      isAccessFormaEnvironment = opt.isAccessFormalEnv
    }
    if (opt.isLogOn == false) {
      log.setOn(opt.isLogOn)
    }
    /*
         if(opt.emotions){
         emotions=opt.emotions;
         webim.Emotions= emotions;
         }
         if(opt.emotionDataIndexs){
         emotionDataIndexs=opt.emotionDataIndexs;
         webim.EmotionDataIndexs= emotionDataIndexs;
         }*/

    if (!loginInfo) {
      if (cbErr) {
        cbErr(tool.getReturnError('loginInfo is empty', -6))
        return
      }
    }
    if (!loginInfo.sdkAppID) {
      if (cbErr) {
        cbErr(tool.getReturnError('loginInfo.sdkAppID is empty', -7))
        return
      }
    }
    if (!loginInfo.accountType) {
      if (cbErr) {
        cbErr(tool.getReturnError('loginInfo.accountType is empty', -8))
        return
      }
    }

    if (loginInfo.identifier) {
      ctx.identifier = loginInfo.identifier.toString()
    }
    if (loginInfo.identifier && !loginInfo.userSig) {
      if (cbErr) {
        cbErr(tool.getReturnError('loginInfo.userSig is empty', -9))
        return
      }
    }
    if (loginInfo.userSig) {
      ctx.userSig = loginInfo.userSig.toString()
    }
    ctx.sdkAppID = loginInfo.sdkAppID
    ctx.accountType = loginInfo.accountType

    if (ctx.identifier && ctx.userSig) {
      //带登录态
      //登录
      proto_login(function(identifierNick, headurl) {
        MsgManager.init(
          listeners,
          function(mmInitResp) {
            if (cbOk) {
              mmInitResp.identifierNick = identifierNick
              mmInitResp.headurl = headurl
              cbOk(mmInitResp)
            }
          },
          cbErr
        )
      }, cbErr)
    } else {
      //不带登录态，进入直播场景sdk
      MsgManager.init(listeners, cbOk, cbErr)
    }
  }

  //初始化浏览器信息
  var initBrowserInfo = function() {
    //初始化浏览器类型
    BROWSER_INFO = tool.getBrowserInfo()
    log.info('BROWSER_INFO: type=' + BROWSER_INFO.type + ', ver=' + BROWSER_INFO.ver)
    if (BROWSER_INFO.type == 'ie') {
      if (parseInt(BROWSER_INFO.ver) < 10) {
        lowerBR = true
      }
    }
  }

  //接口质量上报
  var reportApiQuality = function(cmd, errorCode, errorInfo) {
    if (cmd == 'longpolling' && (errorCode == longPollingTimeOutErrorCode || errorCode == longPollingKickedErrorCode)) {
      //longpolling 返回60008错误可以视为正常,可以不上报
      return
    }
    var eventId = CMD_EVENT_ID_MAP[cmd]
    if (eventId) {
      var reportTime = unixtime()
      var uniqKey = null
      var msgCmdErrorCode = {
        Code: errorCode,
        ErrMsg: errorInfo
      }
      if (ctx.a2) {
        uniqKey = ctx.a2.substring(0, 10) + '_' + reportTime + '_' + createRandom()
      } else if (ctx.userSig) {
        uniqKey = ctx.userSig.substring(0, 10) + '_' + reportTime + '_' + createRandom()
      }

      if (uniqKey) {
        var rptEvtItem = {
          UniqKey: uniqKey,
          EventId: eventId,
          ReportTime: reportTime,
          MsgCmdErrorCode: msgCmdErrorCode
        }

        if (cmd == 'login') {
          var loginApiReportItems = []
          loginApiReportItems.push(rptEvtItem)
          var loginReportOpt = {
            EvtItems: loginApiReportItems,
            MainVersion: SDK.VERSION,
            Version: '0'
          }
          proto_reportApiQuality(
            loginReportOpt,
            function(resp) {
              loginApiReportItems = null //
            },
            function(err) {
              loginApiReportItems = null //
            }
          )
        } else {
          apiReportItems.push(rptEvtItem)
          if (apiReportItems.length >= maxApiReportItemCount) {
            //累计一定条数再上报
            var reportOpt = {
              EvtItems: apiReportItems,
              MainVersion: SDK.VERSION,
              Version: '0'
            }
            proto_reportApiQuality(
              reportOpt,
              function(resp) {
                apiReportItems = [] //清空
              },
              function(err) {
                apiReportItems = [] //清空
              }
            )
          }
        }
      }
    }
  }

  // REST API calls
  //上线
  var proto_login = function(cbOk, cbErr) {
    ConnManager.apiCall(
      SRV_NAME.OPEN_IM,
      'login',
      {
        State: 'Online'
      },
      function(loginResp) {
        if (loginResp.TinyId) {
          ctx.tinyid = loginResp.TinyId
        } else {
          if (cbErr) {
            cbErr(tool.getReturnError('TinyId is empty', -10))
            return
          }
        }
        if (loginResp.A2Key) {
          ctx.a2 = loginResp.A2Key
        } else {
          if (cbErr) {
            cbErr(tool.getReturnError('A2Key is empty', -11))
            return
          }
        }
        var tag_list = ['Tag_Profile_IM_Nick', 'Tag_Profile_IM_Image']
        var options = {
          From_Account: ctx.identifier,
          To_Account: [ctx.identifier],
          LastStandardSequence: 0,
          TagList: tag_list
        }
        proto_getProfilePortrait(
          options,
          function(resp) {
            var nick, image
            if (resp.UserProfileItem && resp.UserProfileItem.length > 0) {
              for (var i in resp.UserProfileItem) {
                for (var j in resp.UserProfileItem[i].ProfileItem) {
                  switch (resp.UserProfileItem[i].ProfileItem[j].Tag) {
                    case 'Tag_Profile_IM_Nick':
                      nick = resp.UserProfileItem[i].ProfileItem[j].Value
                      if (nick) ctx.identifierNick = nick
                      break
                    case 'Tag_Profile_IM_Image':
                      image = resp.UserProfileItem[i].ProfileItem[j].Value
                      if (image) ctx.headurl = image
                      break
                  }
                }
              }
            }
            if (cbOk) cbOk(ctx.identifierNick, ctx.headurl) //回传当前用户昵称
          },
          cbErr
        )
      },
      cbErr
    )
  }
  //下线
  var proto_logout = function(type, cbOk, cbErr) {
    if (!checkLogin(cbErr, false)) {
      //不带登录态
      clearSdk()
      if (cbOk)
        cbOk({
          ActionStatus: ACTION_STATUS.OK,
          ErrorCode: 0,
          ErrorInfo: 'logout success'
        })
      return
    }
    if (type == 'all') {
      ConnManager.apiCall(
        SRV_NAME.OPEN_IM,
        'logout',
        {},
        function(resp) {
          clearSdk()
          if (cbOk) cbOk(resp)
        },
        cbErr
      )
    } else {
      ConnManager.apiCall(
        SRV_NAME.OPEN_IM,
        'longpollinglogout',
        {
          LongPollingId: LongPollingId
        },
        function(resp) {
          clearSdk()
          if (cbOk) cbOk(resp)
        },
        cbErr
      )
    }
  }
  //发送消息，包括私聊和群聊
  var proto_sendMsg = function(msg, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    var msgInfo = null

    switch (msg.sess.type()) {
      case SESSION_TYPE.C2C:
        msgInfo = {
          From_Account: ctx.identifier,
          To_Account: msg.sess.id().toString(),
          MsgTimeStamp: msg.time,
          MsgSeq: msg.seq,
          MsgRandom: msg.random,
          MsgBody: []
        }
        break
      case SESSION_TYPE.GROUP:
        var subType = msg.getSubType()
        msgInfo = {
          GroupId: msg.sess.id().toString(),
          From_Account: ctx.identifier,
          Random: msg.random,
          MsgBody: []
        }
        switch (subType) {
          case GROUP_MSG_SUB_TYPE.COMMON:
            msgInfo.MsgPriority = 'COMMON'
            break
          case GROUP_MSG_SUB_TYPE.REDPACKET:
            msgInfo.MsgPriority = 'REDPACKET'
            break
          case GROUP_MSG_SUB_TYPE.LOVEMSG:
            msgInfo.MsgPriority = 'LOVEMSG'
            break
          case GROUP_MSG_SUB_TYPE.TIP:
            log.error('不能主动发送群提示消息,subType=' + subType)
            break
          default:
            log.error('发送群消息时，出现未知子消息类型：subType=' + subType)
            return
            break
        }
        break
      default:
        break
    }

    for (var i in msg.elems) {
      var elem = msg.elems[i]
      var msgContent = null
      var msgType = elem.type
      switch (msgType) {
        case MSG_ELEMENT_TYPE.TEXT: //文本
          msgContent = {
            Text: elem.content.text
          }
          break
        case MSG_ELEMENT_TYPE.FACE: //表情
          msgContent = {
            Index: elem.content.index,
            Data: elem.content.data
          }
          break
        case MSG_ELEMENT_TYPE.IMAGE: //图片
          var ImageInfoArray = []
          for (var j in elem.content.ImageInfoArray) {
            ImageInfoArray.push({
              Type: elem.content.ImageInfoArray[j].type,
              Size: elem.content.ImageInfoArray[j].size,
              Width: elem.content.ImageInfoArray[j].width,
              Height: elem.content.ImageInfoArray[j].height,
              URL: elem.content.ImageInfoArray[j].url
            })
          }
          msgContent = {
            ImageFormat: elem.content.ImageFormat,
            UUID: elem.content.UUID,
            ImageInfoArray: ImageInfoArray
          }
          break
        case MSG_ELEMENT_TYPE.SOUND: //
          log.warn('web端暂不支持发送语音消息')
          continue
          break
        case MSG_ELEMENT_TYPE.LOCATION: //
          log.warn('web端暂不支持发送地理位置消息')
          continue
          break
        case MSG_ELEMENT_TYPE.FILE: //
          msgContent = {
            UUID: elem.content.uuid,
            FileName: elem.content.name,
            FileSize: elem.content.size,
            DownloadFlag: elem.content.downFlag
          }
          break
        case MSG_ELEMENT_TYPE.CUSTOM: //
          msgContent = {
            Data: elem.content.data,
            Desc: elem.content.desc,
            Ext: elem.content.ext
          }
          msgType = MSG_ELEMENT_TYPE.CUSTOM
          break
        default:
          log.warn('web端暂不支持发送' + elem.type + '消息')
          continue
          break
      }

      if (msg.PushInfoBoolean) {
        msgInfo.OfflinePushInfo = msg.PushInfo //当android终端进程被杀掉时才走push，IOS退到后台即可
      }

      msgInfo.MsgBody.push({
        MsgType: msgType,
        MsgContent: msgContent
      })
    }
    if (msg.sess.type() == SESSION_TYPE.C2C) {
      //私聊
      ConnManager.apiCall(SRV_NAME.OPEN_IM, 'sendmsg', msgInfo, cbOk, cbErr)
    } else if (msg.sess.type() == SESSION_TYPE.GROUP) {
      //群聊
      ConnManager.apiCall(SRV_NAME.GROUP, 'send_group_msg', msgInfo, cbOk, cbErr)
    }
  }
  //长轮询接口
  var proto_longPolling = function(options, cbOk, cbErr) {
    if (!isAccessFormaEnvironment && typeof stopPolling != 'undefined' && stopPolling == true) {
      return
    }
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(SRV_NAME.OPEN_IM, 'longpolling', options, cbOk, cbErr, longPollingDefaultTimeOut, true)
  }

  //长轮询接口(拉取直播聊天室新消息)
  var proto_bigGroupLongPolling = function(options, cbOk, cbErr, timeout) {
    ConnManager.apiCall(SRV_NAME.BIG_GROUP_LONG_POLLING, 'get_msg', options, cbOk, cbErr, timeout)
  }

  //拉取未读c2c消息接口
  var proto_getMsgs = function(cookie, syncFlag, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.OPEN_IM,
      'getmsg',
      {
        Cookie: cookie,
        SyncFlag: syncFlag
      },
      function(resp) {
        if (resp.MsgList && resp.MsgList.length) {
          for (var i in resp.MsgList) {
            tempC2CMsgList.push(resp.MsgList[i])
          }
        }
        if (resp.SyncFlag == 1) {
          proto_getMsgs(resp.Cookie, resp.SyncFlag, cbOk, cbErr)
        } else {
          resp.MsgList = tempC2CMsgList
          tempC2CMsgList = []
          if (cbOk) cbOk(resp)
        }
      },
      cbErr
    )
  }
  //C2C消息已读上报接口
  var proto_c2CMsgReaded = function(cookie, c2CMsgReadedItem, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    var tmpC2CMsgReadedItem = []
    for (var i in c2CMsgReadedItem) {
      var item = {
        To_Account: c2CMsgReadedItem[i].toAccount,
        LastedMsgTime: c2CMsgReadedItem[i].lastedMsgTime
      }
      tmpC2CMsgReadedItem.push(item)
    }
    ConnManager.apiCall(
      SRV_NAME.OPEN_IM,
      'msgreaded',
      {
        C2CMsgReaded: {
          Cookie: cookie,
          C2CMsgReadedItem: tmpC2CMsgReadedItem
        }
      },
      cbOk,
      cbErr
    )
  }

  //删除c2c消息
  var proto_deleteC2CMsg = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(SRV_NAME.OPEN_IM, 'deletemsg', options, cbOk, cbErr)
  }

  //拉取c2c历史消息接口
  var proto_getC2CHistoryMsgs = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.OPEN_IM,
      'getroammsg',
      options,
      function(resp) {
        var reqMsgCount = options.MaxCnt
        var complete = resp.Complete
        var rspMsgCount = resp.MaxCnt
        var msgKey = resp.MsgKey
        var lastMsgTime = resp.LastMsgTime

        if (resp.MsgList && resp.MsgList.length) {
          for (var i in resp.MsgList) {
            tempC2CHistoryMsgList.push(resp.MsgList[i])
          }
        }
        var netxOptions = null
        if (complete == 0) {
          //还有历史消息可拉取
          if (rspMsgCount < reqMsgCount) {
            netxOptions = {
              Peer_Account: options.Peer_Account,
              MaxCnt: reqMsgCount - rspMsgCount,
              LastMsgTime: lastMsgTime,
              MsgKey: msgKey
            }
          }
        }

        if (netxOptions) {
          //继续拉取
          proto_getC2CHistoryMsgs(netxOptions, cbOk, cbErr)
        } else {
          resp.MsgList = tempC2CHistoryMsgList
          tempC2CHistoryMsgList = []
          if (cbOk) cbOk(resp)
        }
      },
      cbErr
    )
  }

  //群组接口
  //创建群组
  //协议参考：https://www.qcloud.com/doc/product/269/1615
  var proto_createGroup = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    var opt = {
      //必填    群组形态，包括Public（公开群），Private（私有群），ChatRoom（聊天室），AVChatRoom（互动直播聊天室）。
      Type: options.Type,
      //必填    群名称，最长30字节。
      Name: options.Name
    }
    var member_list = []

    //Array 选填  初始群成员列表，最多500个。成员信息字段详情参见：群成员资料。
    for (var i = 0; i < options.MemberList.length; i++) {
      member_list.push({
        Member_Account: options.MemberList[i]
      })
    }
    opt.MemberList = member_list
    //选填    为了使得群组ID更加简单，便于记忆传播，腾讯云支持APP在通过REST API创建群组时自定义群组ID。详情参见：自定义群组ID。
    if (options.GroupId) {
      opt.GroupId = options.GroupId
    }
    //选填    群主id，自动添加到群成员中。如果不填，群没有群主。
    if (options.Owner_Account) {
      opt.Owner_Account = options.Owner_Account
    }
    //选填    群简介，最长240字节。
    if (options.Introduction) {
      opt.Introduction = options.Introduction
    }
    //选填    群公告，最长300字节。
    if (options.Notification) {
      opt.Notification = options.Notification
    }
    //选填    最大群成员数量，最大为10000，不填默认为2000个。
    if (options.MaxMemberCount) {
      opt.MaxMemberCount = options.MaxMemberCount
    }
    //选填    申请加群处理方式。包含FreeAccess（自由加入），NeedPermission（需要验证），DisableApply（禁止加群），不填默认为NeedPermission（需要验证）。
    if (options.ApplyJoinOption) {
      //
      opt.ApplyJoinOption = options.ApplyJoinOption
    }
    //Array 选填  群组维度的自定义字段，默认情况是没有的，需要开通，详情参见：自定义字段。
    if (options.AppDefinedData) {
      opt.AppDefinedData = options.AppDefinedData
    }
    //选填    群头像URL，最长100字节。
    if (options.FaceUrl) {
      opt.FaceUrl = options.FaceUrl
    }
    ConnManager.apiCall(SRV_NAME.GROUP, 'create_group', opt, cbOk, cbErr)
  }

  //创建群组-高级接口
  //协议参考：https://www.qcloud.com/doc/product/269/1615
  var proto_createGroupHigh = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(SRV_NAME.GROUP, 'create_group', options, cbOk, cbErr)
  }

  //修改群组基本资料
  //协议参考：https://www.qcloud.com/doc/product/269/1620
  var proto_modifyGroupBaseInfo = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(SRV_NAME.GROUP, 'modify_group_base_info', options, cbOk, cbErr)
  }

  //申请加群
  var proto_applyJoinGroup = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    options.GroupId = String(options.GroupId)
    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'apply_join_group',
      {
        GroupId: options.GroupId,
        ApplyMsg: options.ApplyMsg,
        UserDefinedField: options.UserDefinedField
      },
      cbOk,
      cbErr
    )
  }

  //申请加入大群
  var BigGroupId
  var proto_applyJoinBigGroup = function(options, cbOk, cbErr) {
    options.GroupId = String(options.GroupId)
    BigGroupId = options.GroupId
    var srvName
    if (!checkLogin(cbErr, false)) {
      //未登录
      srvName = SRV_NAME.BIG_GROUP
    } else {
      //已登录
      srvName = SRV_NAME.GROUP
    }
    ConnManager.apiCall(
      srvName,
      'apply_join_group',
      {
        GroupId: options.GroupId,
        ApplyMsg: options.ApplyMsg,
        UserDefinedField: options.UserDefinedField
      },
      function(resp) {
        if (resp.JoinedStatus && resp.JoinedStatus == 'JoinedSuccess') {
          if (resp.LongPollingKey) {
            MsgManager.setBigGroupLongPollingOn(true) //开启长轮询
            MsgManager.setBigGroupLongPollingKey(resp.LongPollingKey) //更新大群长轮询key
            MsgManager.setBigGroupLongPollingMsgMap(options.GroupId, 0) //收到的群消息置0
            MsgManager.bigGroupLongPolling() //开启长轮询
          } else {
            //没有返回LongPollingKey，说明申请加的群不是直播聊天室(AVChatRoom)
            cbErr &&
              cbErr(
                tool.getReturnError(
                  'Join Group succeed; But the type of group is not AVChatRoom: groupid=' + options.GroupId,
                  -12
                )
              )
            return
          }
        }
        if (cbOk) cbOk(resp)
      },
      function(err) {
        if (cbErr) cbErr(err)
      }
    )
  }

  //处理加群申请(同意或拒绝)
  var proto_handleApplyJoinGroupPendency = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'handle_apply_join_group',
      {
        GroupId: options.GroupId,
        Applicant_Account: options.Applicant_Account,
        HandleMsg: options.HandleMsg,
        Authentication: options.Authentication,
        MsgKey: options.MsgKey,
        ApprovalMsg: options.ApprovalMsg,
        UserDefinedField: options.UserDefinedField
      },
      cbOk,
      function(err) {
        if (err.ErrorCode == 10024) {
          //apply has be handled
          if (cbOk) {
            var resp = {
              ActionStatus: ACTION_STATUS.OK,
              ErrorCode: 0,
              ErrorInfo: '该申请已经被处理过'
            }
            cbOk(resp)
          }
        } else {
          if (cbErr) cbErr(err)
        }
      }
    )
  }

  //获取群组未决列表
  var proto_getPendencyGroup = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'get_pendency',
      {
        StartTime: options.StartTime,
        Limit: options.Limit,
        Handle_Account: ctx.identifier
      },
      cbOk,
      function(err) {}
    )
  }

  //群组未决已经上报
  var proto_getPendencyGroupRead = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'report_pendency',
      {
        ReportTime: options.ReportTime,
        From_Account: ctx.identifier
      },
      cbOk,
      function(err) {}
    )
  }

  //处理被邀请进群申请(同意或拒绝)
  var proto_handleInviteJoinGroupRequest = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'handle_invite_join_group',
      {
        GroupId: options.GroupId,
        Inviter_Account: options.Inviter_Account,
        HandleMsg: options.HandleMsg,
        Authentication: options.Authentication,
        MsgKey: options.MsgKey,
        ApprovalMsg: options.ApprovalMsg,
        UserDefinedField: options.UserDefinedField
      },
      cbOk,
      function(err) {}
    )
  }

  //主动退群
  var proto_quitGroup = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'quit_group',
      {
        GroupId: options.GroupId
      },
      cbOk,
      cbErr
    )
  }

  //退出大群
  var proto_quitBigGroup = function(options, cbOk, cbErr) {
    var srvName
    if (!checkLogin(cbErr, false)) {
      //未登录
      srvName = SRV_NAME.BIG_GROUP
    } else {
      //已登录
      srvName = SRV_NAME.GROUP
    }
    MsgManager.resetBigGroupLongPollingInfo()
    ConnManager.apiCall(
      srvName,
      'quit_group',
      {
        GroupId: options.GroupId
      },
      function(resp) {
        MsgStore.delSessByTypeId(SESSION_TYPE.GROUP, options.GroupId)
        //重置当前再请求中的ajax
        //clearXmlHttpObjMap();
        //退出大群成功之后需要重置长轮询信息
        // MsgManager.resetBigGroupLongPollingInfo();
        if (cbOk) cbOk(resp)
      },
      cbErr
    )
  }
  //查找群(按名称)
  var proto_searchGroupByName = function(options, cbOk, cbErr) {
    ConnManager.apiCall(SRV_NAME.GROUP, 'search_group', options, cbOk, cbErr)
  }

  //获取群组公开资料
  var proto_getGroupPublicInfo = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'get_group_public_info',
      {
        GroupIdList: options.GroupIdList,
        ResponseFilter: {
          GroupBasePublicInfoFilter: options.GroupBasePublicInfoFilter
        }
      },
      function(resp) {
        resp.ErrorInfo = ''
        if (resp.GroupInfo) {
          for (var i in resp.GroupInfo) {
            var errorCode = resp.GroupInfo[i].ErrorCode
            if (errorCode > 0) {
              resp.ActionStatus = ACTION_STATUS.FAIL
              resp.GroupInfo[i].ErrorInfo = '[' + errorCode + ']' + resp.GroupInfo[i].ErrorInfo
              resp.ErrorInfo += resp.GroupInfo[i].ErrorInfo + '\n'
            }
          }
        }
        if (resp.ActionStatus == ACTION_STATUS.FAIL) {
          if (cbErr) {
            cbErr(resp)
          }
        } else if (cbOk) {
          cbOk(resp)
        }
      },
      cbErr
    )
  }

  //获取群组详细资料--高级
  //请求协议参考：https://www.qcloud.com/doc/product/269/1616
  var proto_getGroupInfo = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    var opt = {
      GroupIdList: options.GroupIdList,
      ResponseFilter: {
        GroupBaseInfoFilter: options.GroupBaseInfoFilter,
        MemberInfoFilter: options.MemberInfoFilter
      }
    }
    if (options.AppDefinedDataFilter_Group) {
      opt.ResponseFilter.AppDefinedDataFilter_Group = options.AppDefinedDataFilter_Group
    }
    if (options.AppDefinedDataFilter_GroupMember) {
      opt.ResponseFilter.AppDefinedDataFilter_GroupMember = options.AppDefinedDataFilter_GroupMember
    }
    ConnManager.apiCall(SRV_NAME.GROUP, 'get_group_info', opt, cbOk, cbErr)
  }

  //获取群组成员-高级接口
  //协议参考：https://www.qcloud.com/doc/product/269/1617
  var proto_getGroupMemberInfo = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'get_group_member_info',
      {
        GroupId: options.GroupId,
        Offset: options.Offset,
        Limit: options.Limit,
        MemberInfoFilter: options.MemberInfoFilter,
        MemberRoleFilter: options.MemberRoleFilter,
        AppDefinedDataFilter_GroupMember: options.AppDefinedDataFilter_GroupMember
      },
      cbOk,
      cbErr
    )
  }

  //增加群组成员
  //协议参考：https://www.qcloud.com/doc/product/269/1621
  var proto_addGroupMember = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'add_group_member',
      {
        GroupId: options.GroupId,
        Silence: options.Silence,
        MemberList: options.MemberList
      },
      cbOk,
      cbErr
    )
  }
  //修改群组成员资料
  //协议参考：https://www.qcloud.com/doc/product/269/1623
  var proto_modifyGroupMember = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    var opt = {}
    if (options.GroupId) {
      opt.GroupId = options.GroupId
    }
    if (options.Member_Account) {
      opt.Member_Account = options.Member_Account
    }
    //Admin或者Member
    if (options.Role) {
      opt.Role = options.Role
    }
    // AcceptAndNotify代表解收并提示消息，Discard代表不接收也不提示消息，AcceptNotNotify代表接收消息但不提示
    if (options.MsgFlag) {
      opt.MsgFlag = options.MsgFlag
    }
    if (options.ShutUpTime) {
      //禁言时间
      opt.ShutUpTime = options.ShutUpTime
    }
    if (options.NameCard) {
      //群名片,最大不超过50个字节
      opt.NameCard = options.NameCard
    }
    if (options.AppMemberDefinedData) {
      //群成员维度的自定义字段，默认情况是没有的，需要开通
      opt.AppMemberDefinedData = options.AppMemberDefinedData
    }
    ConnManager.apiCall(SRV_NAME.GROUP, 'modify_group_member_info', opt, cbOk, cbErr)
  }
  //删除群组成员
  //协议参考：https://www.qcloud.com/doc/product/269/1622
  var proto_deleteGroupMember = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'delete_group_member',
      {
        GroupId: options.GroupId,
        Silence: options.Silence,
        MemberToDel_Account: options.MemberToDel_Account,
        Reason: options.Reason
      },
      cbOk,
      cbErr
    )
  }
  //解散群组
  //协议参考：https://www.qcloud.com/doc/product/269/1624
  var proto_destroyGroup = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'destroy_group',
      {
        GroupId: options.GroupId
      },
      cbOk,
      cbErr
    )
  }
  //转让群组
  //协议参考：https://www.qcloud.com/doc/product/269/1633
  var proto_changeGroupOwner = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(SRV_NAME.GROUP, 'change_group_owner', options, cbOk, cbErr)
  }
  //获取用户所加入的群组-高级接口
  //协议参考：https://www.qcloud.com/doc/product/269/1625
  var proto_getJoinedGroupListHigh = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'get_joined_group_list',
      {
        Member_Account: options.Member_Account,
        Limit: options.Limit,
        Offset: options.Offset,
        GroupType: options.GroupType,
        ResponseFilter: {
          GroupBaseInfoFilter: options.GroupBaseInfoFilter,
          SelfInfoFilter: options.SelfInfoFilter
        }
      },
      cbOk,
      cbErr
    )
  }
  //查询一组UserId在群中的身份
  //协议参考：https://www.qcloud.com/doc/product/269/1626
  var proto_getRoleInGroup = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'get_role_in_group',
      {
        GroupId: options.GroupId,
        User_Account: options.User_Account
      },
      cbOk,
      cbErr
    )
  }
  //设置取消成员禁言时间
  //协议参考：https://www.qcloud.com/doc/product/269/1627
  var proto_forbidSendMsg = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'forbid_send_msg',
      {
        GroupId: options.GroupId,
        Members_Account: options.Members_Account,
        ShutUpTime: options.ShutUpTime //单位为秒，为0时表示取消禁言
      },
      cbOk,
      cbErr
    )
  }

  //发送自定义群系统通知
  var proto_sendCustomGroupNotify = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(SRV_NAME.GROUP, 'send_group_system_notification', options, cbOk, cbErr)
  }

  //拉取群消息接口
  var proto_getGroupMsgs = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'group_msg_get',
      {
        GroupId: options.GroupId,
        ReqMsgSeq: options.ReqMsgSeq,
        ReqMsgNumber: options.ReqMsgNumber
      },
      cbOk,
      cbErr
    )
  }
  //群消息已读上报接口
  var proto_groupMsgReaded = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.GROUP,
      'msg_read_report',
      {
        GroupId: options.GroupId,
        MsgReadedSeq: options.MsgReadedSeq
      },
      cbOk,
      cbErr
    )
  }
  //end

  //好友接口
  //处理好友接口返回的错误码
  var convertErrorEn2ZhFriend = function(resp) {
    var errorAccount = []
    if (resp.Fail_Account && resp.Fail_Account.length) {
      errorAccount = resp.Fail_Account
    }
    if (resp.Invalid_Account && resp.Invalid_Account.length) {
      for (var k in resp.Invalid_Account) {
        errorAccount.push(resp.Invalid_Account[k])
      }
    }
    if (errorAccount.length) {
      resp.ActionStatus = ACTION_STATUS.FAIL
      resp.ErrorCode = ERROR_CODE_CUSTOM
      resp.ErrorInfo = ''
      for (var i in errorAccount) {
        var failCount = errorAccount[i]
        for (var j in resp.ResultItem) {
          if (resp.ResultItem[j].To_Account == failCount) {
            var resultCode = resp.ResultItem[j].ResultCode
            resp.ResultItem[j].ResultInfo = '[' + resultCode + ']' + resp.ResultItem[j].ResultInfo
            resp.ErrorInfo += resp.ResultItem[j].ResultInfo + '\n'
            break
          }
        }
      }
    }
    return resp
  }
  //添加好友
  var proto_applyAddFriend = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'friend_add',
      {
        From_Account: ctx.identifier,
        AddFriendItem: options.AddFriendItem
      },
      function(resp) {
        var newResp = convertErrorEn2ZhFriend(resp)
        if (newResp.ActionStatus == ACTION_STATUS.FAIL) {
          if (cbErr) cbErr(newResp)
        } else if (cbOk) {
          cbOk(newResp)
        }
      },
      cbErr
    )
  }
  //删除好友
  var proto_deleteFriend = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'friend_delete',
      {
        From_Account: ctx.identifier,
        To_Account: options.To_Account,
        DeleteType: options.DeleteType
      },
      function(resp) {
        var newResp = convertErrorEn2ZhFriend(resp)
        if (newResp.ActionStatus == ACTION_STATUS.FAIL) {
          if (cbErr) cbErr(newResp)
        } else if (cbOk) {
          cbOk(newResp)
        }
      },
      cbErr
    )
  }
  //删除会话
  var proto_deleteChat = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return

    if (options.chatType == 1) {
      ConnManager.apiCall(
        SRV_NAME.DEL_CHAT,
        'delete',
        {
          From_Account: ctx.identifier,
          Type: options.chatType,
          To_Account: options.To_Account
        },
        cbOk,
        cbErr
      )
    } else {
      ConnManager.apiCall(
        SRV_NAME.DEL_CHAT,
        'delete',
        {
          From_Account: ctx.identifier,
          Type: options.chatType,
          ToGroupid: options.To_Account
        },
        cbOk,
        cbErr
      )
    }
  }
  //获取好友申请
  var proto_getPendency = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'pendency_get',
      {
        From_Account: ctx.identifier,
        PendencyType: options.PendencyType,
        StartTime: options.StartTime,
        MaxLimited: options.MaxLimited,
        LastSequence: options.LastSequence
      },
      cbOk,
      cbErr
    )
  }
  //好友申请已读上报
  var proto_getPendencyReport = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'PendencyReport',
      {
        From_Account: ctx.identifier,
        LatestPendencyTimeStamp: options.LatestPendencyTimeStamp
      },
      cbOk,
      cbErr
    )
  }
  //删除好友申请
  var proto_deletePendency = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'pendency_delete',
      {
        From_Account: ctx.identifier,
        PendencyType: options.PendencyType,
        To_Account: options.To_Account
      },
      function(resp) {
        var newResp = convertErrorEn2ZhFriend(resp)
        if (newResp.ActionStatus == ACTION_STATUS.FAIL) {
          if (cbErr) cbErr(newResp)
        } else if (cbOk) {
          cbOk(newResp)
        }
      },
      cbErr
    )
  }
  //处理好友申请
  var proto_responseFriend = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'friend_response',
      {
        From_Account: ctx.identifier,
        ResponseFriendItem: options.ResponseFriendItem
      },
      function(resp) {
        var newResp = convertErrorEn2ZhFriend(resp)
        if (newResp.ActionStatus == ACTION_STATUS.FAIL) {
          if (cbErr) cbErr(newResp)
        } else if (cbOk) {
          cbOk(newResp)
        }
      },
      cbErr
    )
  }
  //我的好友
  var proto_getAllFriend = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'friend_get_all',
      {
        From_Account: ctx.identifier,
        TimeStamp: options.TimeStamp,
        StartIndex: options.StartIndex,
        GetCount: options.GetCount,
        LastStandardSequence: options.LastStandardSequence,
        TagList: options.TagList
      },
      cbOk,
      cbErr
    )
  }

  //资料接口
  //查看个人资料
  var proto_getProfilePortrait = function(options, cbOk, cbErr) {
    if (options.To_Account.length > 100) {
      options.To_Account.length = 100
      log.error('获取用户资料人数不能超过100人')
    }
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.PROFILE,
      'portrait_get',
      {
        From_Account: ctx.identifier,
        To_Account: options.To_Account,
        //'LastStandardSequence':options.LastStandardSequence,
        TagList: options.TagList
      },
      function(resp) {
        var errorAccount = []
        if (resp.Fail_Account && resp.Fail_Account.length) {
          errorAccount = resp.Fail_Account
        }
        if (resp.Invalid_Account && resp.Invalid_Account.length) {
          for (var k in resp.Invalid_Account) {
            errorAccount.push(resp.Invalid_Account[k])
          }
        }
        if (errorAccount.length) {
          resp.ActionStatus = ACTION_STATUS.FAIL
          resp.ErrorCode = ERROR_CODE_CUSTOM
          resp.ErrorInfo = ''
          for (var i in errorAccount) {
            var failCount = errorAccount[i]
            for (var j in resp.UserProfileItem) {
              if (resp.UserProfileItem[j].To_Account == failCount) {
                var resultCode = resp.UserProfileItem[j].ResultCode
                resp.UserProfileItem[j].ResultInfo = '[' + resultCode + ']' + resp.UserProfileItem[j].ResultInfo
                resp.ErrorInfo += '账号:' + failCount + ',' + resp.UserProfileItem[j].ResultInfo + '\n'
                break
              }
            }
          }
        }
        if (resp.ActionStatus == ACTION_STATUS.FAIL) {
          if (cbErr) cbErr(resp)
        } else if (cbOk) {
          cbOk(resp)
        }
      },
      cbErr
    )
  }

  //设置个人资料
  var proto_setProfilePortrait = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.PROFILE,
      'portrait_set',
      {
        From_Account: ctx.identifier,
        ProfileItem: options.ProfileItem
      },
      function(resp) {
        for (var i in options.ProfileItem) {
          var profile = options.ProfileItem[i]
          if (profile.Tag == 'Tag_Profile_IM_Nick') {
            ctx.identifierNick = profile.Value //更新昵称
            break
          }
        }
        if (cbOk) cbOk(resp)
      },
      cbErr
    )
  }

  //增加黑名单
  var proto_addBlackList = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'black_list_add',
      {
        From_Account: ctx.identifier,
        To_Account: options.To_Account
      },
      function(resp) {
        var newResp = convertErrorEn2ZhFriend(resp)
        if (newResp.ActionStatus == ACTION_STATUS.FAIL) {
          if (cbErr) cbErr(newResp)
        } else if (cbOk) {
          cbOk(newResp)
        }
      },
      cbErr
    )
  }

  //删除黑名单
  var proto_deleteBlackList = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'black_list_delete',
      {
        From_Account: ctx.identifier,
        To_Account: options.To_Account
      },
      function(resp) {
        var newResp = convertErrorEn2ZhFriend(resp)
        if (newResp.ActionStatus == ACTION_STATUS.FAIL) {
          if (cbErr) cbErr(newResp)
        } else if (cbOk) {
          cbOk(newResp)
        }
      },
      cbErr
    )
  }

  //我的黑名单
  var proto_getBlackList = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.FRIEND,
      'black_list_get',
      {
        From_Account: ctx.identifier,
        StartIndex: options.StartIndex,
        MaxLimited: options.MaxLimited,
        LastSequence: options.LastSequence
      },
      cbOk,
      cbErr
    )
  }

  //获取最近联系人
  var proto_getRecentContactList = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.RECENT_CONTACT,
      'get',
      {
        From_Account: ctx.identifier,
        Count: options.Count
      },
      cbOk,
      cbErr
    )
  }

  //上传图片或文件
  var proto_uploadPic = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    var cmdName
    if (isAccessFormalEnv()) {
      cmdName = 'pic_up'
    } else {
      cmdName = 'pic_up_test'
    }
    ConnManager.apiCall(
      SRV_NAME.PIC,
      cmdName,
      {
        App_Version: VERSION_INFO.APP_VERSION,
        From_Account: ctx.identifier,
        To_Account: options.To_Account,
        Seq: options.Seq,
        Timestamp: options.Timestamp,
        Random: options.Random,
        File_Str_Md5: options.File_Str_Md5,
        File_Size: options.File_Size,
        File_Type: options.File_Type,
        Server_Ver: VERSION_INFO.SERVER_VERSION,
        Auth_Key: authkey,
        Busi_Id: options.Busi_Id,
        PkgFlag: options.PkgFlag,
        Slice_Offset: options.Slice_Offset,
        Slice_Size: options.Slice_Size,
        Slice_Data: options.Slice_Data,
        content_type: 'application/x-www-form-urlencoded'
      },
      cbOk,
      cbErr
    )
  }

  //获取语音和文件下载IP和authkey
  var proto_getIpAndAuthkey = function(cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(SRV_NAME.OPEN_IM, 'authkey', {}, cbOk, cbErr)
  }

  //接口质量上报
  var proto_reportApiQuality = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(SRV_NAME.IM_OPEN_STAT, 'web_report', options, cbOk, cbErr)
  }

  var proto_getLongPollingId = function(options, cbOk, cbErr) {
    if (!checkLogin(cbErr, true)) return
    ConnManager.apiCall(
      SRV_NAME.OPEN_IM,
      'getlongpollingid',
      {},
      function(resp) {
        cbOk && cbOk(resp)
      },
      cbErr
    )
  }

  var proto_applyDownload = function(options, cbOk, cbErr) {
    //把下载地址push到map中
    ConnManager.apiCall(SRV_NAME.PIC, 'apply_download', options, cbOk, cbErr)
  }

  //end
  initBrowserInfo()
  // singleton object ConnManager
  var ConnManager =
    lowerBR == false
      ? new function() {
          var onConnCallback = null //回调函数
          this.init = function(onConnNotify, cbOk, cbErr) {
            if (onConnNotify) onConnCallback = onConnNotify
          }
          this.callBack = function(info) {
            if (onConnCallback) onConnCallback(info)
          }
          this.clear = function() {
            onConnCallback = null
          }
          //请求后台服务接口
          this.apiCall = function(type, cmd, data, cbOk, cbErr, timeout, isLongPolling) {
            //封装后台服务接口地址
            var url = getApiUrl(type, cmd, cbOk, cbErr)
            if (url == false) return
            //发起ajax请求
            var content_type = data.content_type ? data.content_type : 'application/json;charset=utf-8'
            ajaxRequestJson(
              'POST',
              url,
              data,
              timeout,
              content_type,
              isLongPolling,
              function(resp) {
                var errorCode = null,
                  tempErrorInfo = ''
                if (cmd == 'pic_up') {
                  data.Slice_Data = ''
                }
                var info =
                  '\n request url: \n' +
                  url +
                  '\n request body: \n' +
                  JSON.stringify(data) +
                  '\n response: \n' +
                  JSON.stringify(resp)
                //成功
                if (resp.ActionStatus == ACTION_STATUS.OK) {
                  log.info('[' + type + '][' + cmd + ']success: ' + info)
                  if (cbOk) cbOk(resp) //回调
                  errorCode = 0
                  tempErrorInfo = ''
                } else {
                  errorCode = resp.ErrorCode
                  tempErrorInfo = resp.ErrorInfo
                  //报错
                  if (cbErr) {
                    resp.SrcErrorInfo = resp.ErrorInfo
                    resp.ErrorInfo = '[' + type + '][' + cmd + ']failed: ' + info
                    if (cmd != 'longpolling' || resp.ErrorCode != longPollingTimeOutErrorCode) {
                      log.error(resp.ErrorInfo)
                    }
                    cbErr(resp)
                  }
                }
                reportApiQuality(cmd, errorCode, tempErrorInfo) //接口质量上报
              },
              function(err) {
                cbErr && cbErr(err)
                reportApiQuality(cmd, err.ErrorCode, err.ErrorInfo) //接口质量上报
              }
            )
          }
        }()
      : new function() {
          var onConnCallback = null //回调函数
          this.init = function(onConnNotify, cbOk, cbErr) {
            if (onConnNotify) onConnCallback = onConnNotify
          }
          this.callBack = function(info) {
            if (onConnCallback) onConnCallback(info)
          }
          this.clear = function() {
            onConnCallback = null
          }
          //请求后台服务接口
          this.apiCall = function(type, cmd, data, cbOk, cbErr, timeout, isLongPolling) {
            //封装后台服务接口地址
            var url = getApiUrl(type, cmd, cbOk, cbErr)
            if (url == false) return
            //发起jsonp请求
            var reqId = jsonpRequestId++,
              cbkey = 'jsonpcallback', // the 'callback' key
              cbval = 'jsonpRequest' + reqId, // the 'callback' value
              script = document.createElement('script'),
              loaded = 0

            window[cbval] = jsonpCallback
            script.type = 'text/javascript'
            url = url + '&' + cbkey + '=' + cbval + '&jsonpbody=' + encodeURIComponent(JSON.stringify(data))
            script.src = url
            script.async = true

            if (typeof script.onreadystatechange !== 'undefined') {
              // need this for IE due to out-of-order onreadystatechange(), binding script
              // execution to an event listener gives us control over when the script
              // is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
              script.event = 'onclick'
              script.htmlFor = script.id = '_jsonpRequest_' + reqId
            }

            script.onload = script.onreadystatechange = function() {
              if ((this.readyState && this.readyState !== 'complete' && this.readyState !== 'loaded') || loaded) {
                return false
              }
              script.onload = script.onreadystatechange = null
              script.onclick && script.onclick()
              // Call the user callback with the last value stored and clean up values and scripts.
              var resp = jsonpLastRspData
              var info =
                '\n request url: \n' +
                url +
                '\n request body: \n' +
                JSON.stringify(data) +
                '\n response: \n' +
                JSON.stringify(resp)
              if (resp.ActionStatus == ACTION_STATUS.OK) {
                log.info('[' + type + '][' + cmd + ']success: ' + info)
                cbOk && cbOk(resp)
              } else {
                resp.ErrorInfo = '[' + type + '][' + cmd + ']failed ' + info
                if (cmd != 'longpolling' || resp.ErrorCode != longPollingTimeOutErrorCode) {
                  log.error(resp.ErrorInfo)
                } else {
                  log.warn('[' + type + '][' + cmd + ']success: ' + info)
                }
                cbErr && cbErr(resp)
              }
              jsonpLastRspData = undefined
              document.body.removeChild(script)
              loaded = 1
            }

            // Add the script to the DOM head
            document.body.appendChild(script)
          }
        }()
  // class Session
  var Session = function(type, id, name, icon, time, seq) {
    this._impl = {
      skey: Session.skey(type, id),
      type: type,
      id: id,
      name: name,
      icon: icon,
      unread: 0, //未读消息数
      isAutoRead: false,
      time: time >= 0 ? time : 0,
      curMaxMsgSeq: seq >= 0 ? seq : 0,
      msgs: [],
      isFinished: 1
    }
  }
  Session.skey = function(type, id) {
    return type + id
  }
  Session.prototype.type = function() {
    return this._impl.type
  }
  Session.prototype.id = function() {
    return this._impl.id
  }
  Session.prototype.name = function() {
    return this._impl.name
  }
  Session.prototype.icon = function() {
    return this._impl.icon
  }
  Session.prototype.unread = function(val) {
    if (typeof val !== 'undefined') {
      this._impl.unread = val
    } else {
      return this._impl.unread
    }
  }
  Session.prototype.isFinished = function(val) {
    if (typeof val !== 'undefined') {
      this._impl.isFinished = val
    } else {
      return this._impl.isFinished
    }
  }
  Session.prototype.time = function() {
    return this._impl.time
  }
  Session.prototype.curMaxMsgSeq = function(seq) {
    if (typeof seq !== 'undefined') {
      this._impl.curMaxMsgSeq = seq
    } else {
      return this._impl.curMaxMsgSeq
    }
  }
  Session.prototype.msgCount = function() {
    return this._impl.msgs.length
  }
  Session.prototype.msg = function(index) {
    return this._impl.msgs[index]
  }
  Session.prototype.msgs = function() {
    return this._impl.msgs
  }
  Session.prototype._impl_addMsg = function(msg) {
    this._impl.msgs.push(msg)
    //if (!msg.isSend && msg.time > this._impl.time)
    if (msg.time > this._impl.time) this._impl.time = msg.time
    //if (!msg.isSend && msg.seq > this._impl.curMaxMsgSeq)
    if (msg.seq > this._impl.curMaxMsgSeq) this._impl.curMaxMsgSeq = msg.seq
    //自己发送的消息不计入未读数
    if (!msg.isSend && !this._impl.isAutoRead) {
      this._impl.unread++
    }
  }
  //class C2CMsgReadedItem
  var C2CMsgReadedItem = function(toAccount, lastedMsgTime) {
    this.toAccount = toAccount
    this.lastedMsgTime = lastedMsgTime
  }

  var calcUniqId = function(num1, num2) {
    var str1 = parseInt(num1).toString(2) + '00000000000000000000000000000000'
    var str2 = parseInt(num2).toString(2)
    var arr1 = str1.split('').reverse()
    var arr2 = str2.split('').reverse()
    var res = []
    var length = arr1.length > arr2.length ? arr1.length : arr2.length
    for (var a = 0; a < length; a++) {
      var sig = Number(arr1[a] || 0) || Number(arr2[a] || 0)
      res.push(sig)
    }
    var numstr = res.reverse().join('')
    var long = {
      high: '0x' + parseInt(numstr.substr(0, numstr.length - 32), 2).toString(16),
      low: '0x' + parseInt(numstr.substr(numstr.length - 32 - 1), 2).toString(16)
    }
    var longVal = new Long(long.low, long.high, true)
    return longVal.toString()
  }
  // class Msg
  var Msg = function(sess, isSend, seq, random, time, fromAccount, subType, fromAccountNick) {
    this.sess = sess
    this.subType = subType >= 0 ? subType : 0 //消息类型,c2c消息时，type取值为0；group消息时，type取值0和1，0-普通群消息，1-群提示消息
    this.fromAccount = fromAccount
    this.fromAccountNick = fromAccountNick ? fromAccountNick : fromAccount
    this.isSend = Boolean(isSend)
    this.seq = seq >= 0 ? seq : nextSeq()
    this.random = random >= 0 ? random : createRandom()
    this.time = time >= 0 ? time : unixtime()
    this.elems = []
    var type = sess.type()
    switch (type) {
      case SESSION_TYPE.GROUP:
        this.uniqueId = calcUniqId(this.seq, this.random)
        break
      case SESSION_TYPE.C2C:
      default:
        this.uniqueId = calcUniqId(this.time, this.random)
        break
    }
  }
  Msg.prototype.getSession = function() {
    return this.sess
  }
  Msg.prototype.getType = function() {
    return this.subType
  }
  Msg.prototype.getSubType = function() {
    return this.subType
  }
  Msg.prototype.getFromAccount = function() {
    return this.fromAccount
  }
  Msg.prototype.getFromAccountNick = function() {
    return this.fromAccountNick
  }
  Msg.prototype.getIsSend = function() {
    return this.isSend
  }
  Msg.prototype.getSeq = function() {
    return this.seq
  }
  Msg.prototype.getTime = function() {
    return this.time
  }
  Msg.prototype.getRandom = function() {
    return this.random
  }
  Msg.prototype.getElems = function() {
    return this.elems
  }
  Msg.prototype.getMsgUniqueId = function() {
    return this.uniqueId
  }
  //文本
  Msg.prototype.addText = function(text) {
    this.addElem(new webim.Msg.Elem(MSG_ELEMENT_TYPE.TEXT, text))
  }
  //表情
  Msg.prototype.addFace = function(face) {
    this.addElem(new webim.Msg.Elem(MSG_ELEMENT_TYPE.FACE, face))
  }
  //图片
  Msg.prototype.addImage = function(image) {
    this.addElem(new webim.Msg.Elem(MSG_ELEMENT_TYPE.IMAGE, image))
  }
  //地理位置
  Msg.prototype.addLocation = function(location) {
    this.addElem(new webim.Msg.Elem(MSG_ELEMENT_TYPE.LOCATION, location))
  }
  //文件
  Msg.prototype.addFile = function(file) {
    this.addElem(new webim.Msg.Elem(MSG_ELEMENT_TYPE.FILE, file))
  }
  //自定义
  Msg.prototype.addCustom = function(custom) {
    this.addElem(new webim.Msg.Elem(MSG_ELEMENT_TYPE.CUSTOM, custom))
  }
  Msg.prototype.addElem = function(elem) {
    this.elems.push(elem)
  }
  Msg.prototype.toHtml = function() {
    var html = ''
    for (var i in this.elems) {
      var elem = this.elems[i]
      html += elem.toHtml()
    }
    return html
  }

  // class Msg.Elem
  Msg.Elem = function(type, value) {
    this.type = type
    this.content = value
  }
  Msg.Elem.prototype.getType = function() {
    return this.type
  }
  Msg.Elem.prototype.getContent = function() {
    return this.content
  }
  Msg.Elem.prototype.toHtml = function() {
    var html
    html = this.content.toHtml()
    return html
  }

  // class Msg.Elem.Text
  Msg.Elem.Text = function(text) {
    this.text = tool.xssFilter(text)
  }
  Msg.Elem.Text.prototype.getText = function() {
    return this.text
  }
  Msg.Elem.Text.prototype.toHtml = function() {
    return this.text
  }

  // class Msg.Elem.Face
  Msg.Elem.Face = function(index, data) {
    this.index = index
    this.data = data
  }
  Msg.Elem.Face.prototype.getIndex = function() {
    return this.index
  }
  Msg.Elem.Face.prototype.getData = function() {
    return this.data
  }
  Msg.Elem.Face.prototype.toHtml = function() {
    var faceUrl = null
    var index = emotionDataIndexs[this.data]
    var emotion = emotions[index]
    if (emotion && emotion[1]) {
      faceUrl = emotion[1]
    }
    if (faceUrl) {
      return "<img src='" + faceUrl + "'/>"
    } else {
      return this.data
    }
  }

  // 地理位置消息 class Msg.Elem.Location
  Msg.Elem.Location = function(longitude, latitude, desc) {
    this.latitude = latitude //纬度
    this.longitude = longitude //经度
    this.desc = desc //描述
  }
  Msg.Elem.Location.prototype.getLatitude = function() {
    return this.latitude
  }
  Msg.Elem.Location.prototype.getLongitude = function() {
    return this.longitude
  }
  Msg.Elem.Location.prototype.getDesc = function() {
    return this.desc
  }
  Msg.Elem.Location.prototype.toHtml = function() {
    return '经度=' + this.longitude + ',纬度=' + this.latitude + ',描述=' + this.desc
  }

  //图片消息
  // class Msg.Elem.Images
  Msg.Elem.Images = function(imageId, format) {
    this.UUID = imageId
    if (typeof format !== 'number') {
      format = parseInt(IMAGE_FORMAT[format] || IMAGE_FORMAT['UNKNOWN'], 10)
    }
    this.ImageFormat = format
    this.ImageInfoArray = []
  }
  Msg.Elem.Images.prototype.addImage = function(image) {
    this.ImageInfoArray.push(image)
  }
  Msg.Elem.Images.prototype.toHtml = function() {
    var smallImage = this.getImage(IMAGE_TYPE.SMALL)
    var bigImage = this.getImage(IMAGE_TYPE.LARGE)
    var oriImage = this.getImage(IMAGE_TYPE.ORIGIN)
    if (!bigImage) {
      bigImage = smallImage
    }
    if (!oriImage) {
      oriImage = smallImage
    }
    return (
      "<img src='" +
      smallImage.getUrl() +
      '#' +
      bigImage.getUrl() +
      '#' +
      oriImage.getUrl() +
      "' style='CURSOR: hand' id='" +
      this.getImageId() +
      "' bigImgUrl='" +
      bigImage.getUrl() +
      "' onclick='imageClick(this)' />"
    )
  }
  Msg.Elem.Images.prototype.getImageId = function() {
    return this.UUID
  }
  Msg.Elem.Images.prototype.getImageFormat = function() {
    return this.ImageFormat
  }
  Msg.Elem.Images.prototype.getImage = function(type) {
    for (var i in this.ImageInfoArray) {
      if (this.ImageInfoArray[i].getType() == type) {
        return this.ImageInfoArray[i]
      }
    }
    var img = null
    this.ImageInfoArray.forEach(function(image) {
      if (image.getType() == type) {
        img = image
      }
    })
    return img
  }
  // class Msg.Elem.Images.Image
  Msg.Elem.Images.Image = function(type, size, width, height, url) {
    this.type = type
    this.size = size
    this.width = width
    this.height = height
    this.url = url
  }
  Msg.Elem.Images.Image.prototype.getType = function() {
    return this.type
  }
  Msg.Elem.Images.Image.prototype.getSize = function() {
    return this.size
  }
  Msg.Elem.Images.Image.prototype.getWidth = function() {
    return this.width
  }
  Msg.Elem.Images.Image.prototype.getHeight = function() {
    return this.height
  }
  Msg.Elem.Images.Image.prototype.getUrl = function() {
    return this.url
  }

  // class Msg.Elem.Sound
  Msg.Elem.Sound = function(uuid, second, size, senderId, receiverId, downFlag, chatType) {
    this.uuid = uuid //文件id
    this.second = second //时长，单位：秒
    this.size = size //大小，单位：字节
    this.senderId = senderId //发送者
    this.receiverId = receiverId //接收方id
    this.downFlag = downFlag //下载标志位
    this.busiId = chatType == SESSION_TYPE.C2C ? 2 : 1 //busi_id ( 1：群    2:C2C)

    //根据不同情况拉取数据
    //是否需要申请下载地址  0:到架平申请  1:到cos申请  2:不需要申请, 直接拿url下载
    if (downFlag !== undefined && busiId !== undefined) {
      getFileDownUrlV2(uuid, senderId, second, downFlag, receiverId, this.busiId, UPLOAD_RES_TYPE.SOUND)
    } else {
      this.downUrl = getSoundDownUrl(uuid, senderId, second) //下载地址
    }
  }
  Msg.Elem.Sound.prototype.getUUID = function() {
    return this.uuid
  }
  Msg.Elem.Sound.prototype.getSecond = function() {
    return this.second
  }
  Msg.Elem.Sound.prototype.getSize = function() {
    return this.size
  }
  Msg.Elem.Sound.prototype.getSenderId = function() {
    return this.senderId
  }
  Msg.Elem.Sound.prototype.getDownUrl = function() {
    return this.downUrl
  }
  Msg.Elem.Sound.prototype.toHtml = function() {
    if (BROWSER_INFO.type == 'ie' && parseInt(BROWSER_INFO.ver) <= 8) {
      return '[这是一条语音消息]demo暂不支持ie8(含)以下浏览器播放语音,语音URL:' + this.downUrl
    }
    return (
      '<audio id="uuid_' +
      this.uuid +
      '" src="' +
      this.downUrl +
      '" controls="controls" onplay="onChangePlayAudio(this)" preload="none"></audio>'
    )
  }

  // class Msg.Elem.File
  Msg.Elem.File = function(uuid, name, size, senderId, receiverId, downFlag, chatType) {
    this.uuid = uuid //文件id
    this.name = name //文件名
    this.size = size //大小，单位：字节
    this.senderId = senderId //发送者
    this.receiverId = receiverId //接收方id
    this.downFlag = downFlag //下载标志位

    this.busiId = chatType == SESSION_TYPE.C2C ? 2 : 1 //busi_id ( 1：群    2:C2C)
    //根据不同情况拉取数据
    //是否需要申请下载地址  0:到架平申请  1:到cos申请  2:不需要申请, 直接拿url下载
    if (downFlag !== undefined && busiId !== undefined) {
      getFileDownUrlV2(uuid, senderId, name, downFlag, receiverId, this.busiId, UPLOAD_RES_TYPE.FILE)
    } else {
      this.downUrl = getFileDownUrl(uuid, senderId, name) //下载地址
    }
  }
  Msg.Elem.File.prototype.getUUID = function() {
    return this.uuid
  }
  Msg.Elem.File.prototype.getName = function() {
    return this.name
  }
  Msg.Elem.File.prototype.getSize = function() {
    return this.size
  }
  Msg.Elem.File.prototype.getSenderId = function() {
    return this.senderId
  }
  Msg.Elem.File.prototype.getDownUrl = function() {
    return this.downUrl
  }
  Msg.Elem.File.prototype.getDownFlag = function() {
    return this.downFlag
  }
  Msg.Elem.File.prototype.toHtml = function() {
    var fileSize, unitStr
    fileSize = this.size
    unitStr = 'Byte'
    if (this.size >= 1024) {
      fileSize = Math.round(this.size / 1024)
      unitStr = 'KB'
    }
    return (
      '<a href="javascript" onclick="webim.onDownFile("' +
      this.uuid +
      '")" title="点击下载文件" ><i class="glyphicon glyphicon-file">&nbsp;' +
      this.name +
      '(' +
      fileSize +
      unitStr +
      ')</i></a>'
    )
  }

  // class Msg.Elem.GroupTip 群提示消息对象
  Msg.Elem.GroupTip = function(opType, opUserId, groupId, groupName, userIdList, userinfo) {
    this.opType = opType //操作类型
    this.opUserId = opUserId //操作者id
    this.groupId = groupId //群id
    this.groupName = groupName //群名称
    this.userIdList = userIdList ? userIdList : [] //被操作的用户id列表
    this.groupInfoList = [] //新的群资料信息，群资料变更时才有值
    this.memberInfoList = [] //新的群成员资料信息，群成员资料变更时才有值
    this.groupMemberNum = null //群成员数，操作类型为加群或者退群时才有值
    this.userinfo = userinfo ? userinfo : [] //被操作的用户信息列表列表
  }
  Msg.Elem.GroupTip.prototype.addGroupInfo = function(groupInfo) {
    this.groupInfoList.push(groupInfo)
  }
  Msg.Elem.GroupTip.prototype.addMemberInfo = function(memberInfo) {
    this.memberInfoList.push(memberInfo)
  }
  Msg.Elem.GroupTip.prototype.getOpType = function() {
    return this.opType
  }
  Msg.Elem.GroupTip.prototype.getOpUserId = function() {
    return this.opUserId
  }
  Msg.Elem.GroupTip.prototype.getGroupId = function() {
    return this.groupId
  }
  Msg.Elem.GroupTip.prototype.getGroupName = function() {
    return this.groupName
  }
  Msg.Elem.GroupTip.prototype.getUserIdList = function() {
    return this.userIdList
  }
  Msg.Elem.GroupTip.prototype.getUserInfo = function() {
    return this.userinfo
  }
  Msg.Elem.GroupTip.prototype.getGroupInfoList = function() {
    return this.groupInfoList
  }
  Msg.Elem.GroupTip.prototype.getMemberInfoList = function() {
    return this.memberInfoList
  }
  Msg.Elem.GroupTip.prototype.getGroupMemberNum = function() {
    return this.groupMemberNum
  }
  Msg.Elem.GroupTip.prototype.setGroupMemberNum = function(groupMemberNum) {
    return (this.groupMemberNum = groupMemberNum)
  }
  Msg.Elem.GroupTip.prototype.toHtml = function() {
    var text = '[群提示消息]'
    var maxIndex = GROUP_TIP_MAX_USER_COUNT - 1
    switch (this.opType) {
      case GROUP_TIP_TYPE.JOIN: //加入群
        text += this.opUserId + '邀请了'
        for (var m in this.userIdList) {
          text += this.userIdList[m] + ','
          if (this.userIdList.length > GROUP_TIP_MAX_USER_COUNT && m == maxIndex) {
            text += '等' + this.userIdList.length + '人'
            break
          }
        }
        text += '加入该群'
        break
      case GROUP_TIP_TYPE.QUIT: //退出群
        text += this.opUserId + '主动退出该群'
        break
      case GROUP_TIP_TYPE.KICK: //踢出群
        text += this.opUserId + '将'
        for (var m in this.userIdList) {
          text += this.userIdList[m] + ','
          if (this.userIdList.length > GROUP_TIP_MAX_USER_COUNT && m == maxIndex) {
            text += '等' + this.userIdList.length + '人'
            break
          }
        }
        text += '踢出该群'
        break
      case GROUP_TIP_TYPE.SET_ADMIN: //设置管理员
        text += this.opUserId + '将'
        for (var m in this.userIdList) {
          text += this.userIdList[m] + ','
          if (this.userIdList.length > GROUP_TIP_MAX_USER_COUNT && m == maxIndex) {
            text += '等' + this.userIdList.length + '人'
            break
          }
        }
        text += '设为管理员'
        break
      case GROUP_TIP_TYPE.CANCEL_ADMIN: //取消管理员
        text += this.opUserId + '取消'
        for (var m in this.userIdList) {
          text += this.userIdList[m] + ','
          if (this.userIdList.length > GROUP_TIP_MAX_USER_COUNT && m == maxIndex) {
            text += '等' + this.userIdList.length + '人'
            break
          }
        }
        text += '的管理员资格'
        break

      case GROUP_TIP_TYPE.MODIFY_GROUP_INFO: //群资料变更
        text += this.opUserId + '修改了群资料：'
        for (var m in this.groupInfoList) {
          var type = this.groupInfoList[m].getType()
          var value = this.groupInfoList[m].getValue()
          switch (type) {
            case GROUP_TIP_MODIFY_GROUP_INFO_TYPE.FACE_URL:
              text += '群头像为' + value + '; '
              break
            case GROUP_TIP_MODIFY_GROUP_INFO_TYPE.NAME:
              text += '群名称为' + value + '; '
              break
            case GROUP_TIP_MODIFY_GROUP_INFO_TYPE.OWNER:
              text += '群主为' + value + '; '
              break
            case GROUP_TIP_MODIFY_GROUP_INFO_TYPE.NOTIFICATION:
              text += '群公告为' + value + '; '
              break
            case GROUP_TIP_MODIFY_GROUP_INFO_TYPE.INTRODUCTION:
              text += '群简介为' + value + '; '
              break
            default:
              text += '未知信息为:type=' + type + ',value=' + value + '; '
              break
          }
        }
        break

      case GROUP_TIP_TYPE.MODIFY_MEMBER_INFO: //群成员资料变更(禁言时间)
        text += this.opUserId + '修改了群成员资料:'
        for (var m in this.memberInfoList) {
          var userId = this.memberInfoList[m].getUserId()
          var shutupTime = this.memberInfoList[m].getShutupTime()
          text += userId + ': '
          if (shutupTime != null && shutupTime !== undefined) {
            if (shutupTime == 0) {
              text += '取消禁言; '
            } else {
              text += '禁言' + shutupTime + '秒; '
            }
          } else {
            text += ' shutupTime为空'
          }
          if (this.memberInfoList.length > GROUP_TIP_MAX_USER_COUNT && m == maxIndex) {
            text += '等' + this.memberInfoList.length + '人'
            break
          }
        }
        break

      case GROUP_TIP_TYPE.READED: //消息已读
        /**/
        Log.info('消息已读同步')
        break
      default:
        text += '未知群提示消息类型：type=' + this.opType
        break
    }
    return text
  }

  // class Msg.Elem.GroupTip.GroupInfo，变更的群资料信息对象
  Msg.Elem.GroupTip.GroupInfo = function(type, value) {
    this.type = type //群资料信息类型
    this.value = value //对应的值
  }
  Msg.Elem.GroupTip.GroupInfo.prototype.getType = function() {
    return this.type
  }
  Msg.Elem.GroupTip.GroupInfo.prototype.getValue = function() {
    return this.value
  }

  // class Msg.Elem.GroupTip.MemberInfo，变更的群成员资料信息对象
  Msg.Elem.GroupTip.MemberInfo = function(userId, shutupTime) {
    this.userId = userId //群成员id
    this.shutupTime = shutupTime //群成员被禁言时间，0表示取消禁言，大于0表示被禁言时长，单位：秒
  }
  Msg.Elem.GroupTip.MemberInfo.prototype.getUserId = function() {
    return this.userId
  }
  Msg.Elem.GroupTip.MemberInfo.prototype.getShutupTime = function() {
    return this.shutupTime
  }

  // 自定义消息类型 class Msg.Elem.Custom
  Msg.Elem.Custom = function(data, desc, ext) {
    this.data = data //数据
    this.desc = desc //描述
    this.ext = ext //扩展字段
  }
  Msg.Elem.Custom.prototype.getData = function() {
    return this.data
  }
  Msg.Elem.Custom.prototype.getDesc = function() {
    return this.desc
  }
  Msg.Elem.Custom.prototype.getExt = function() {
    return this.ext
  }
  Msg.Elem.Custom.prototype.toHtml = function() {
    return this.data
  }

  // singleton object MsgStore
  var MsgStore = new function() {
    var sessMap = {} //跟所有用户或群的聊天记录MAP
    var sessTimeline = [] //按时间降序排列的会话列表
    msgCache = {} //消息缓存，用于判重
    //C2C
    this.cookie = '' //上一次拉取新c2c消息的cookie
    this.syncFlag = 0 //上一次拉取新c2c消息的是否继续拉取标记

    var visitSess = function(visitor) {
      for (var i in sessMap) {
        visitor(sessMap[i])
      }
    }
    //消息查重
    var checkDupMsg = function(msg) {
      var dup = false
      var first_key = msg.sess._impl.skey
      var second_key = msg.isSend + msg.seq + msg.random
      var tempMsg = msgCache[first_key] && msgCache[first_key][second_key]
      if (tempMsg) {
        dup = true
      }
      if (msgCache[first_key]) {
        msgCache[first_key][second_key] = {
          time: msg.time
        }
      } else {
        msgCache[first_key] = {}
        msgCache[first_key][second_key] = {
          time: msg.time
        }
      }
      return dup
    }

    this.sessMap = function() {
      return sessMap
    }
    this.sessCount = function() {
      return sessTimeline.length
    }
    this.sessByTypeId = function(type, id) {
      var skey = Session.skey(type, id)
      if (skey === undefined || skey == null) return null
      return sessMap[skey]
    }
    this.delSessByTypeId = function(type, id) {
      var skey = Session.skey(type, id)
      if (skey === undefined || skey == null) return false
      if (sessMap[skey]) {
        delete sessMap[skey]
        delete msgCache[skey]
      }
      return true
    }
    this.resetCookieAndSyncFlag = function() {
      this.cookie = ''
      this.syncFlag = 0
    }

    //切换将当前会话的自动读取消息标志为isOn,重置其他会话的自动读取消息标志为false
    this.setAutoRead = function(selSess, isOn, isResetAll) {
      if (isResetAll)
        visitSess(function(s) {
          s._impl.isAutoRead = false
        })
      if (selSess) {
        selSess._impl.isAutoRead = isOn //
        if (isOn) {
          //是否调用已读上报接口
          selSess._impl.unread = 0

          if (selSess._impl.type == SESSION_TYPE.C2C) {
            //私聊消息已读上报
            var tmpC2CMsgReadedItem = []
            tmpC2CMsgReadedItem.push(new C2CMsgReadedItem(selSess._impl.id, selSess._impl.time))
            //调用C2C消息已读上报接口
            proto_c2CMsgReaded(
              MsgStore.cookie,
              tmpC2CMsgReadedItem,
              function(resp) {
                log.info('[setAutoRead]: c2CMsgReaded success')
              },
              function(err) {
                log.error('[setAutoRead}: c2CMsgReaded failed:' + err.ErrorInfo)
              }
            )
          } else if (selSess._impl.type == SESSION_TYPE.GROUP) {
            //群聊消息已读上报
            var tmpOpt = {
              GroupId: selSess._impl.id,
              MsgReadedSeq: selSess._impl.curMaxMsgSeq
            }
            //调用group消息已读上报接口
            proto_groupMsgReaded(
              tmpOpt,
              function(resp) {
                log.info('groupMsgReaded success')
              },
              function(err) {
                log.error('groupMsgReaded failed:' + err.ErrorInfo)
              }
            )
          }
        }
      }
    }

    this.c2CMsgReaded = function(opts, cbOk, cbErr) {
      var tmpC2CMsgReadedItem = []
      tmpC2CMsgReadedItem.push(new C2CMsgReadedItem(opts.To_Account, opts.LastedMsgTime))
      //调用C2C消息已读上报接口
      proto_c2CMsgReaded(
        MsgStore.cookie,
        tmpC2CMsgReadedItem,
        function(resp) {
          if (cbOk) {
            log.info('c2CMsgReaded success')
            cbOk(resp)
          }
        },
        function(err) {
          if (cbErr) {
            log.error('c2CMsgReaded failed:' + err.ErrorInfo)
            cbErr(err)
          }
        }
      )
    }

    this.addSession = function(sess) {
      sessMap[sess._impl.skey] = sess
    }
    this.delSession = function(sess) {
      delete sessMap[sess._impl.skey]
    }
    this.clear = function() {
      sessMap = {} //跟所有用户或群的聊天记录MAP
      sessTimeline = [] //按时间降序排列的会话列表
      msgCache = {} //消息缓存，用于判重
      this.cookie = '' //上一次拉取新c2c消息的cookie
      this.syncFlag = 0 //上一次拉取新c2c消息的是否继续拉取标记
    }
    this.addMsg = function(msg) {
      if (checkDupMsg(msg)) return false
      var sess = msg.sess
      if (!sessMap[sess._impl.skey]) this.addSession(sess)
      sess._impl_addMsg(msg)
      return true
    }
    this.updateTimeline = function() {
      var arr = new Array()
      visitSess(function(sess) {
        arr.push(sess)
      })
      arr.sort(function(a, b) {
        return b.time - a.time
      })
      sessTimeline = arr
    }
  }()
  // singleton object MsgManager
  var MsgManager = new function() {
    var onMsgCallback = null //新消息(c2c和group)回调

    var onGroupInfoChangeCallback = null //群资料变化回调
    //收到新群系统消息回调列表
    var onGroupSystemNotifyCallbacks = {
      '1': null,
      '2': null,
      '3': null,
      '4': null,
      '5': null,
      '6': null,
      '7': null,
      '8': null,
      '9': null,
      '10': null,
      '11': null,
      '15': null,
      '255': null,
      '12': null
    }
    //监听好友系统通知函数
    var onFriendSystemNotifyCallbacks = {
      '1': null,
      '2': null,
      '3': null,
      '4': null,
      '5': null,
      '6': null,
      '7': null,
      '8': null
    }

    var onProfileSystemNotifyCallbacks = {
      '1': null
    }

    var onKickedEventCall = null

    var onMsgReadCallback = null

    //普通长轮询
    var longPollingOn = false //是否开启普通长轮询
    var isLongPollingRequesting = false //是否在长轮询ing
    var notifySeq = 0 //c2c通知seq
    var noticeSeq = 0 //群消息seq

    //大群长轮询
    var onBigGroupMsgCallback = null //大群消息回调
    var bigGroupLongPollingOn = false //是否开启长轮询
    var bigGroupLongPollingStartSeq = 0 //请求拉消息的起始seq(大群长轮询)
    var bigGroupLongPollingHoldTime = 90 //客户端长轮询的超时时间，单位是秒(大群长轮询)
    var bigGroupLongPollingKey = null //客户端加入群组后收到的的Key(大群长轮询)
    var bigGroupLongPollingMsgMap = {} //记录收到的群消息数
    var onC2cEventCallbacks = {
      '92': null, //消息已读通知,
      '96': null
    }
    var onAppliedDownloadUrl = null

    var getLostGroupMsgCount = 0 //补拉丢失的群消息次数
    //我的群当前最大的seq
    var myGroupMaxSeqs = {} //用于补拉丢失的群消息

    var groupSystemMsgsCache = {} //群组系统消息缓存,用于判重

    //设置长轮询开关
    //isOn=true 开启
    //isOn=false 停止
    this.setLongPollingOn = function(isOn) {
      longPollingOn = isOn
    }
    this.getLongPollingOn = function() {
      return longPollingOn
    }

    //重置长轮询变量
    this.resetLongPollingInfo = function() {
      longPollingOn = false
      notifySeq = 0
      noticeSeq = 0
    }

    //设置大群长轮询开关
    //isOn=true 开启
    //isOn=false 停止
    this.setBigGroupLongPollingOn = function(isOn) {
      bigGroupLongPollingOn = isOn
    }
    //设置大群长轮询key
    this.setBigGroupLongPollingKey = function(key) {
      bigGroupLongPollingKey = key
    }
    //重置大群长轮询变量
    this.resetBigGroupLongPollingInfo = function() {
      bigGroupLongPollingOn = false
      bigGroupLongPollingStartSeq = 0
      bigGroupLongPollingKey = null
      bigGroupLongPollingMsgMap = {}
    }

    //设置群消息数据条数
    this.setBigGroupLongPollingMsgMap = function(groupId, count) {
      var bigGroupLongPollingMsgCount = bigGroupLongPollingMsgMap[groupId]
      if (bigGroupLongPollingMsgCount) {
        bigGroupLongPollingMsgCount = parseInt(bigGroupLongPollingMsgCount) + count
        bigGroupLongPollingMsgMap[groupId] = bigGroupLongPollingMsgCount
      } else {
        bigGroupLongPollingMsgMap[groupId] = count
      }
    }

    //重置
    this.clear = function() {
      onGroupInfoChangeCallback = null
      onGroupSystemNotifyCallbacks = {
        '1': null, //申请加群请求（只有管理员会收到）
        '2': null, //申请加群被同意（只有申请人能够收到）
        '3': null, //申请加群被拒绝（只有申请人能够收到）
        '4': null, //被管理员踢出群(只有被踢者接收到)
        '5': null, //群被解散(全员接收)
        '6': null, //创建群(创建者接收)
        '7': null, //邀请加群(被邀请者接收)
        '8': null, //主动退群(主动退出者接收)
        '9': null, //设置管理员(被设置者接收)
        '10': null, //取消管理员(被取消者接收)
        '11': null, //群已被回收(全员接收)
        '15': null, //群已被回收(全员接收)
        '255': null, //用户自定义通知(默认全员接收)
        '12': null //邀请加群(被邀请者需要同意)
      }
      onFriendSystemNotifyCallbacks = {
        '1': null, //好友表增加
        '2': null, //好友表删除
        '3': null, //未决增加
        '4': null, //未决删除
        '5': null, //黑名单增加
        '6': null, //黑名单删除
        '7': null, //未决已读上报
        '8': null //好友信息(备注，分组)变更
      }
      onProfileSystemNotifyCallbacks = {
        '1': null //资料修改
      }
      //重置普通长轮询参数
      onMsgCallback = null
      longPollingOn = false
      notifySeq = 0 //c2c新消息通知seq
      noticeSeq = 0 //group新消息seq

      //重置大群长轮询参数
      onBigGroupMsgCallback = null
      bigGroupLongPollingOn = false
      bigGroupLongPollingStartSeq = 0
      bigGroupLongPollingKey = null
      bigGroupLongPollingMsgMap = {}

      groupSystemMsgsCache = {}

      ipList = [] //文件下载地址
      authkey = null //文件下载票据
      expireTime = null //票据超时时间
    }

    //初始化文件下载ip和票据
    var initIpAndAuthkey = function(cbOk, cbErr) {
      proto_getIpAndAuthkey(
        function(resp) {
          ipList = resp.IpList
          authkey = resp.AuthKey
          expireTime = resp.ExpireTime
          if (cbOk) cbOk(resp)
        },
        function(err) {
          log.error('initIpAndAuthkey failed:' + err.ErrorInfo)
          if (cbErr) cbErr(err)
        }
      )
    }

    //初始化我的群当前最大的seq，用于补拉丢失的群消息
    var initMyGroupMaxSeqs = function(cbOk, cbErr) {
      var opts = {
        Member_Account: ctx.identifier,
        Limit: 1000,
        Offset: 0,
        GroupBaseInfoFilter: ['NextMsgSeq']
      }
      proto_getJoinedGroupListHigh(
        opts,
        function(resp) {
          if (!resp.GroupIdList || resp.GroupIdList.length == 0) {
            log.info('initMyGroupMaxSeqs: 目前还没有加入任何群组')
            if (cbOk) cbOk(resp)
            return
          }
          for (var i = 0; i < resp.GroupIdList.length; i++) {
            var group_id = resp.GroupIdList[i].GroupId
            var curMaxSeq = resp.GroupIdList[i].NextMsgSeq - 1
            myGroupMaxSeqs[group_id] = curMaxSeq
          }

          if (cbOk) cbOk(resp)
        },
        function(err) {
          log.error('initMyGroupMaxSeqs failed:' + err.ErrorInfo)
          if (cbErr) cbErr(err)
        }
      )
    }

    //补拉群消息
    var getLostGroupMsgs = function(groupId, reqMsgSeq, reqMsgNumber) {
      getLostGroupMsgCount++
      //发起一个拉群群消息请求
      var tempOpts = {
        GroupId: groupId,
        ReqMsgSeq: reqMsgSeq,
        ReqMsgNumber: reqMsgNumber
      }
      //发起一个拉群群消息请求
      log.warn('第' + getLostGroupMsgCount + '次补齐群消息,参数=' + JSON.stringify(tempOpts))
      MsgManager.syncGroupMsgs(tempOpts)
    }

    //更新群当前最大消息seq
    var updateMyGroupCurMaxSeq = function(groupId, msgSeq) {
      //更新myGroupMaxSeqs中的群最大seq
      var curMsgSeq = myGroupMaxSeqs[groupId]
      if (curMsgSeq) {
        //如果存在，比较大小
        if (msgSeq > curMsgSeq) {
          myGroupMaxSeqs[groupId] = msgSeq
        }
      } else {
        //不存在，新增
        myGroupMaxSeqs[groupId] = msgSeq
      }
    }

    //添加群消息列表
    var addGroupMsgList = function(msgs, new_group_msgs) {
      for (var p in msgs) {
        var newGroupMsg = msgs[p]
        //发群消息时，长轮询接口会返回用户自己发的群消息
        //if(newGroupMsg.From_Account && newGroupMsg.From_Account!=ctx.identifier ){
        if (newGroupMsg.From_Account) {
          //false-不是主动拉取的历史消息
          //true-需要保存到sdk本地session,并且需要判重
          var msg = handlerGroupMsg(newGroupMsg, false, true)
          if (msg) {
            //不为空，加到新消息里
            new_group_msgs.push(msg)
          }
          //更新myGroupMaxSeqs中的群最大seq
          updateMyGroupCurMaxSeq(newGroupMsg.ToGroupId, newGroupMsg.MsgSeq)
        }
      }
      return new_group_msgs
    }

    //处理收到的群普通和提示消息
    var handlerOrdinaryAndTipC2cMsgs = function(eventType, groupMsgArray) {
      var groupMsgMap = {} //保存收到的C2c消息信息（群号，最小，最大消息seq，消息列表）
      var new_group_msgs = []
      var minGroupMsgSeq = 99999999
      var maxGroupMsgSeq = -1
      for (var j in groupMsgArray) {
        var groupMsgs = groupMsgMap[groupMsgArray[j].ToGroupId]
        if (!groupMsgs) {
          groupMsgs = groupMsgMap[groupMsgArray[j].ToGroupId] = {
            min: minGroupMsgSeq, //收到新消息最小seq
            max: maxGroupMsgSeq, //收到新消息最大seq
            msgs: [] //收到的新消息
          }
        }
        //更新长轮询的群NoticeSeq
        if (groupMsgArray[j].NoticeSeq > noticeSeq) {
          log.warn('noticeSeq=' + noticeSeq + ',msgNoticeSeq=' + groupMsgArray[j].NoticeSeq)
          noticeSeq = groupMsgArray[j].NoticeSeq
        }
        groupMsgArray[j].Event = eventType
        groupMsgMap[groupMsgArray[j].ToGroupId].msgs.push(groupMsgArray[j]) //新增一条消息
        if (groupMsgArray[j].MsgSeq < groupMsgs.min) {
          //记录最小的消息seq
          groupMsgMap[groupMsgArray[j].ToGroupId].min = groupMsgArray[j].MsgSeq
        }
        if (groupMsgArray[j].MsgSeq > groupMsgs.max) {
          //记录最大的消息seq
          groupMsgMap[groupMsgArray[j].ToGroupId].max = groupMsgArray[j].MsgSeq
        }
      }

      for (var groupId in groupMsgMap) {
        var tempCount = groupMsgMap[groupId].max - groupMsgMap[groupId].min + 1 //收到的新的群消息数
        var curMaxMsgSeq = myGroupMaxSeqs[groupId] //获取本地保存的群最大消息seq
        if (curMaxMsgSeq) {
          //存在这个群的最大消息seq
          //高并发情况下，长轮询可能存在丢消息，这时需要客户端通过拉取群消息接口补齐下
          //1、如果收到的新消息最小seq比当前最大群消息seq大于1，则表示收到的群消息发生跳跃，需要补齐
          //2、收到的新群消息seq存在不连续情况，也需要补齐
          if (groupMsgMap[groupId].min - curMaxMsgSeq > 1 || groupMsgMap[groupId].msgs.length < tempCount) {
            //发起一个拉群群消息请求
            log.warn(
              '发起一次补齐群消息请求,curMaxMsgSeq=' +
                curMaxMsgSeq +
                ', minMsgSeq=' +
                groupMsgMap[groupId].min +
                ', maxMsgSeq=' +
                groupMsgMap[groupId].max +
                ', msgs.length=' +
                groupMsgMap[groupId].msgs.length +
                ', tempCount=' +
                tempCount
            )
            getLostGroupMsgs(groupId, groupMsgMap[groupId].max, groupMsgMap[groupId].max - curMaxMsgSeq)
            //更新myGroupMaxSeqs中的群最大seq
            updateMyGroupCurMaxSeq(groupId, groupMsgMap[groupId].max)
          } else {
            new_group_msgs = addGroupMsgList(groupMsgMap[groupId].msgs, new_group_msgs)
          }
        } else {
          //不存在该群的最大消息seq
          log.warn('不存在该群的最大消息seq，群id=' + groupId)
          //高并发情况下，长轮询可能存在丢消息，这时需要客户端通过拉取群消息接口补齐下
          //1、收到的新群消息seq存在不连续情况，也需要补齐
          if (groupMsgMap[groupId].msgs.length < tempCount) {
            //发起一个拉群群消息请求
            log.warn(
              '发起一次补齐群消息请求,minMsgSeq=' +
                groupMsgMap[groupId].min +
                ', maxMsgSeq=' +
                groupMsgMap[groupId].max +
                ', msgs.length=' +
                groupMsgMap[groupId].msgs.length +
                ', tempCount=' +
                tempCount
            )
            getLostGroupMsgs(groupId, groupMsgMap[groupId].max, tempCount)
            //更新myGroupMaxSeqs中的群最大seq
            updateMyGroupCurMaxSeq(groupId, groupMsgMap[groupId].max)
          } else {
            new_group_msgs = addGroupMsgList(groupMsgMap[groupId].msgs, new_group_msgs)
          }
        }
      }
      if (new_group_msgs.length) {
        MsgStore.updateTimeline()
      }
      if (onMsgCallback && new_group_msgs.length) onMsgCallback(new_group_msgs)
    }

    //处理收到的群普通和提示消息
    var handlerOrdinaryAndTipGroupMsgs = function(eventType, groupMsgArray) {
      var groupMsgMap = {} //保存收到的群消息信息（群号，最小，最大消息seq，消息列表）
      var new_group_msgs = []
      var minGroupMsgSeq = 99999999
      var maxGroupMsgSeq = -1
      for (var j in groupMsgArray) {
        var groupMsgs = groupMsgMap[groupMsgArray[j].ToGroupId]
        if (!groupMsgs) {
          groupMsgs = groupMsgMap[groupMsgArray[j].ToGroupId] = {
            min: minGroupMsgSeq, //收到新消息最小seq
            max: maxGroupMsgSeq, //收到新消息最大seq
            msgs: [] //收到的新消息
          }
        }
        //更新长轮询的群NoticeSeq
        if (groupMsgArray[j].NoticeSeq > noticeSeq) {
          log.warn('noticeSeq=' + noticeSeq + ',msgNoticeSeq=' + groupMsgArray[j].NoticeSeq)
          noticeSeq = groupMsgArray[j].NoticeSeq
        }
        groupMsgArray[j].Event = eventType
        groupMsgMap[groupMsgArray[j].ToGroupId].msgs.push(groupMsgArray[j]) //新增一条消息
        if (groupMsgArray[j].MsgSeq < groupMsgs.min) {
          //记录最小的消息seq
          groupMsgMap[groupMsgArray[j].ToGroupId].min = groupMsgArray[j].MsgSeq
        }
        if (groupMsgArray[j].MsgSeq > groupMsgs.max) {
          //记录最大的消息seq
          groupMsgMap[groupMsgArray[j].ToGroupId].max = groupMsgArray[j].MsgSeq
        }
      }

      for (var groupId in groupMsgMap) {
        var tempCount = groupMsgMap[groupId].max - groupMsgMap[groupId].min + 1 //收到的新的群消息数
        var curMaxMsgSeq = myGroupMaxSeqs[groupId] //获取本地保存的群最大消息seq
        if (curMaxMsgSeq) {
          //存在这个群的最大消息seq
          //高并发情况下，长轮询可能存在丢消息，这时需要客户端通过拉取群消息接口补齐下
          //1、如果收到的新消息最小seq比当前最大群消息seq大于1，则表示收到的群消息发生跳跃，需要补齐
          //2、收到的新群消息seq存在不连续情况，也需要补齐
          if (groupMsgMap[groupId].min - curMaxMsgSeq > 1 || groupMsgMap[groupId].msgs.length < tempCount) {
            //发起一个拉群群消息请求
            log.warn(
              '发起一次补齐群消息请求,curMaxMsgSeq=' +
                curMaxMsgSeq +
                ', minMsgSeq=' +
                groupMsgMap[groupId].min +
                ', maxMsgSeq=' +
                groupMsgMap[groupId].max +
                ', msgs.length=' +
                groupMsgMap[groupId].msgs.length +
                ', tempCount=' +
                tempCount
            )
            getLostGroupMsgs(groupId, groupMsgMap[groupId].max, groupMsgMap[groupId].max - curMaxMsgSeq)
            //更新myGroupMaxSeqs中的群最大seq
            updateMyGroupCurMaxSeq(groupId, groupMsgMap[groupId].max)
          } else {
            new_group_msgs = addGroupMsgList(groupMsgMap[groupId].msgs, new_group_msgs)
          }
        } else {
          //不存在该群的最大消息seq
          log.warn('不存在该群的最大消息seq，群id=' + groupId)
          //高并发情况下，长轮询可能存在丢消息，这时需要客户端通过拉取群消息接口补齐下
          //1、收到的新群消息seq存在不连续情况，也需要补齐
          if (groupMsgMap[groupId].msgs.length < tempCount) {
            //发起一个拉群群消息请求
            log.warn(
              '发起一次补齐群消息请求,minMsgSeq=' +
                groupMsgMap[groupId].min +
                ', maxMsgSeq=' +
                groupMsgMap[groupId].max +
                ', msgs.length=' +
                groupMsgMap[groupId].msgs.length +
                ', tempCount=' +
                tempCount
            )
            getLostGroupMsgs(groupId, groupMsgMap[groupId].max, tempCount)
            //更新myGroupMaxSeqs中的群最大seq
            updateMyGroupCurMaxSeq(groupId, groupMsgMap[groupId].max)
          } else {
            new_group_msgs = addGroupMsgList(groupMsgMap[groupId].msgs, new_group_msgs)
          }
        }
      }
      if (new_group_msgs.length) {
        MsgStore.updateTimeline()
      }
      if (onMsgCallback && new_group_msgs.length) onMsgCallback(new_group_msgs)
    }

    //处理新的群提示消息
    var handlerGroupTips = function(groupTips) {
      var new_group_msgs = []
      for (var o in groupTips) {
        var groupTip = groupTips[o]
        //添加event字段
        groupTip.Event = LONG_POLLINNG_EVENT_TYPE.GROUP_TIP
        //更新群消息通知seq
        if (groupTip.NoticeSeq > noticeSeq) {
          noticeSeq = groupTip.NoticeSeq
        }
        var msg = handlerGroupMsg(groupTip, false, true)
        if (msg) {
          new_group_msgs.push(msg)
        }
      }
      if (new_group_msgs.length) {
        MsgStore.updateTimeline()
      }
      if (onMsgCallback && new_group_msgs.length) onMsgCallback(new_group_msgs)
    }

    //处理新的群系统消息
    //isNeedValidRepeatMsg 是否需要判重
    var handlerGroupSystemMsgs = function(groupSystemMsgs, isNeedValidRepeatMsg) {
      for (var k in groupSystemMsgs) {
        var groupTip = groupSystemMsgs[k]
        var groupReportTypeMsg = groupTip.MsgBody
        var reportType = groupReportTypeMsg.ReportType
        //当长轮询返回的群系统消息，才需要更新群消息通知seq
        if (isNeedValidRepeatMsg == false && groupTip.NoticeSeq && groupTip.NoticeSeq > noticeSeq) {
          noticeSeq = groupTip.NoticeSeq
        }
        var toAccount = groupTip.GroupInfo.To_Account
        //过滤本不应该给自己的系统消息
        /*if (!toAccount || toAccount != ctx.identifier) {
                 log.error("收到本不应该给自己的系统消息: To_Account=" + toAccount);
                 continue;
                 }*/
        if (isNeedValidRepeatMsg) {
          //var key=groupTip.ToGroupId+"_"+reportType+"_"+groupTip.MsgTimeStamp+"_"+groupReportTypeMsg.Operator_Account;
          var key = groupTip.ToGroupId + '_' + reportType + '_' + groupReportTypeMsg.Operator_Account
          var isExist = groupSystemMsgsCache[key]
          if (isExist) {
            log.warn('收到重复的群系统消息：key=' + key)
            continue
          }
          groupSystemMsgsCache[key] = true
        }

        var notify = {
          SrcFlag: 0,
          ReportType: reportType,
          GroupId: groupTip.ToGroupId,
          GroupName: groupTip.GroupInfo.GroupName,
          Operator_Account: groupReportTypeMsg.Operator_Account,
          MsgTime: groupTip.MsgTimeStamp,
          groupReportTypeMsg: groupReportTypeMsg
        }
        switch (reportType) {
          case GROUP_SYSTEM_TYPE.JOIN_GROUP_REQUEST: //申请加群(只有管理员会接收到)
            notify['RemarkInfo'] = groupReportTypeMsg.RemarkInfo
            notify['MsgKey'] = groupReportTypeMsg.MsgKey
            notify['Authentication'] = groupReportTypeMsg.Authentication
            notify['UserDefinedField'] = groupTip.UserDefinedField
            notify['From_Account'] = groupTip.From_Account
            notify['MsgSeq'] = groupTip.ClientSeq
            notify['MsgRandom'] = groupTip.MsgRandom
            break
          case GROUP_SYSTEM_TYPE.JOIN_GROUP_ACCEPT: //申请加群被同意(只有申请人自己接收到)
          case GROUP_SYSTEM_TYPE.JOIN_GROUP_REFUSE: //申请加群被拒绝(只有申请人自己接收到)
            notify['RemarkInfo'] = groupReportTypeMsg.RemarkInfo
            break
          case GROUP_SYSTEM_TYPE.KICK: //被管理员踢出群(只有被踢者接收到)
          case GROUP_SYSTEM_TYPE.DESTORY: //群被解散(全员接收)
          case GROUP_SYSTEM_TYPE.CREATE: //创建群(创建者接收, 不展示)
          case GROUP_SYSTEM_TYPE.INVITED_JOIN_GROUP_REQUEST: //邀请加群(被邀请者接收)
          case GROUP_SYSTEM_TYPE.INVITED_JOIN_GROUP_REQUEST_AGREE: //邀请加群(被邀请者需同意)
          case GROUP_SYSTEM_TYPE.QUIT: //主动退群(主动退出者接收, 不展示)
          case GROUP_SYSTEM_TYPE.SET_ADMIN: //群设置管理员(被设置者接收)
          case GROUP_SYSTEM_TYPE.CANCEL_ADMIN: //取消管理员(被取消者接收)
          case GROUP_SYSTEM_TYPE.REVOKE: //群已被回收(全员接收, 不展示)
            break
          case GROUP_SYSTEM_TYPE.READED: //群消息已读同步
            break
          case GROUP_SYSTEM_TYPE.CUSTOM: //用户自定义通知(默认全员接收)
            notify['MsgSeq'] = groupTip.MsgSeq
            notify['UserDefinedField'] = groupReportTypeMsg.UserDefinedField
            break
          default:
            log.error('未知群系统消息类型：reportType=' + reportType)
            break
        }

        if (isNeedValidRepeatMsg) {
          //注释只收取一种通知
          // if (reportType == GROUP_SYSTEM_TYPE.JOIN_GROUP_REQUEST) {
          //回调
          if (onGroupSystemNotifyCallbacks[reportType]) {
            onGroupSystemNotifyCallbacks[reportType](notify)
          } else {
            log.error('未知群系统消息类型：reportType=' + reportType)
          }
          //}
        } else {
          //回调
          if (onGroupSystemNotifyCallbacks[reportType]) {
            if (reportType == GROUP_SYSTEM_TYPE.READED) {
              var arr = notify.groupReportTypeMsg.GroupReadInfoArray
              for (var i = 0, l = arr.length; i < l; i++) {
                var item = arr[i]
                onGroupSystemNotifyCallbacks[reportType](item)
              }
            } else {
              onGroupSystemNotifyCallbacks[reportType](notify)
            }
          }
        }
      } //loop
    }

    //处理新的好友系统通知
    //isNeedValidRepeatMsg 是否需要判重
    var handlerFriendSystemNotices = function(friendSystemNotices, isNeedValidRepeatMsg) {
      var friendNotice, type, notify
      for (var k in friendSystemNotices) {
        friendNotice = friendSystemNotices[k]
        type = friendNotice.PushType
        //当长轮询返回的群系统消息，才需要更新通知seq
        if (isNeedValidRepeatMsg == false && friendNotice.NoticeSeq && friendNotice.NoticeSeq > noticeSeq) {
          noticeSeq = friendNotice.NoticeSeq
        }
        notify = {
          Type: type
        }
        switch (type) {
          case FRIEND_NOTICE_TYPE.FRIEND_ADD: //好友表增加
            notify['Accounts'] = friendNotice.FriendAdd_Account
            break
          case FRIEND_NOTICE_TYPE.FRIEND_DELETE: //好友表删除
            notify['Accounts'] = friendNotice.FriendDel_Account
            break
          case FRIEND_NOTICE_TYPE.PENDENCY_ADD: //未决增加
            notify['PendencyList'] = friendNotice.PendencyAdd
            break
          case FRIEND_NOTICE_TYPE.PENDENCY_DELETE: //未决删除
            notify['Accounts'] = friendNotice.FrienPencydDel_Account
            break
          case FRIEND_NOTICE_TYPE.BLACK_LIST_ADD: //黑名单增加
            notify['Accounts'] = friendNotice.BlackListAdd_Account
            break
          case FRIEND_NOTICE_TYPE.BLACK_LIST_DELETE: //黑名单删除
            notify['Accounts'] = friendNotice.BlackListDel_Account
            break
          /*case FRIEND_NOTICE_TYPE.PENDENCY_REPORT://未决已读上报

                     break;
                     case FRIEND_NOTICE_TYPE.FRIEND_UPDATE://好友数据更新

                     break;
                     */
          default:
            log.error('未知好友系统通知类型：friendNotice=' + JSON.stringify(friendNotice))
            break
        }

        if (isNeedValidRepeatMsg) {
          if (type == FRIEND_NOTICE_TYPE.PENDENCY_ADD) {
            //回调
            if (onFriendSystemNotifyCallbacks[type]) onFriendSystemNotifyCallbacks[type](notify)
          }
        } else {
          //回调
          if (onFriendSystemNotifyCallbacks[type]) onFriendSystemNotifyCallbacks[type](notify)
        }
      } //loop
    }

    //处理新的资料系统通知
    //isNeedValidRepeatMsg 是否需要判重
    var handlerProfileSystemNotices = function(profileSystemNotices, isNeedValidRepeatMsg) {
      var profileNotice, type, notify
      for (var k in profileSystemNotices) {
        profileNotice = profileSystemNotices[k]
        type = profileNotice.PushType
        //当长轮询返回的群系统消息，才需要更新通知seq
        if (isNeedValidRepeatMsg == false && profileNotice.NoticeSeq && profileNotice.NoticeSeq > noticeSeq) {
          noticeSeq = profileNotice.NoticeSeq
        }
        notify = {
          Type: type
        }
        switch (type) {
          case PROFILE_NOTICE_TYPE.PROFILE_MODIFY: //资料修改
            notify['Profile_Account'] = profileNotice.Profile_Account
            notify['ProfileList'] = profileNotice.ProfileList
            break
          default:
            log.error('未知资料系统通知类型：profileNotice=' + JSON.stringify(profileNotice))
            break
        }

        if (isNeedValidRepeatMsg) {
          if (type == PROFILE_NOTICE_TYPE.PROFILE_MODIFY) {
            //回调
            if (onProfileSystemNotifyCallbacks[type]) onProfileSystemNotifyCallbacks[type](notify)
          }
        } else {
          //回调
          if (onProfileSystemNotifyCallbacks[type]) onProfileSystemNotifyCallbacks[type](notify)
        }
      } //loop
    }

    //处理新的群系统消息(用于直播大群长轮询)
    var handlerGroupSystemMsg = function(groupTip) {
      var groupReportTypeMsg = groupTip.MsgBody
      var reportType = groupReportTypeMsg.ReportType
      var toAccount = groupTip.GroupInfo.To_Account
      //过滤本不应该给自己的系统消息
      //if(!toAccount || toAccount!=ctx.identifier){
      //    log.error("收到本不应该给自己的系统消息: To_Account="+toAccount);
      //    continue;
      //}
      var notify = {
        SrcFlag: 1,
        ReportType: reportType,
        GroupId: groupTip.ToGroupId,
        GroupName: groupTip.GroupInfo.GroupName,
        Operator_Account: groupReportTypeMsg.Operator_Account,
        MsgTime: groupTip.MsgTimeStamp
      }
      switch (reportType) {
        case GROUP_SYSTEM_TYPE.JOIN_GROUP_REQUEST: //申请加群(只有管理员会接收到)
          notify['RemarkInfo'] = groupReportTypeMsg.RemarkInfo
          notify['MsgKey'] = groupReportTypeMsg.MsgKey
          notify['Authentication'] = groupReportTypeMsg.Authentication
          notify['UserDefinedField'] = groupTip.UserDefinedField
          notify['From_Account'] = groupTip.From_Account
          notify['MsgSeq'] = groupTip.ClientSeq
          notify['MsgRandom'] = groupTip.MsgRandom
          break
        case GROUP_SYSTEM_TYPE.JOIN_GROUP_ACCEPT: //申请加群被同意(只有申请人自己接收到)
        case GROUP_SYSTEM_TYPE.JOIN_GROUP_REFUSE: //申请加群被拒绝(只有申请人自己接收到)
          notify['RemarkInfo'] = groupReportTypeMsg.RemarkInfo
          break
        case GROUP_SYSTEM_TYPE.KICK: //被管理员踢出群(只有被踢者接收到)
        case GROUP_SYSTEM_TYPE.DESTORY: //群被解散(全员接收)
        case GROUP_SYSTEM_TYPE.CREATE: //创建群(创建者接收, 不展示)
        case GROUP_SYSTEM_TYPE.INVITED_JOIN_GROUP_REQUEST: //邀请加群(被邀请者接收)
        case GROUP_SYSTEM_TYPE.INVITED_JOIN_GROUP_REQUEST_AGREE: //邀请加群(被邀请者需要同意)
        case GROUP_SYSTEM_TYPE.QUIT: //主动退群(主动退出者接收, 不展示)
        case GROUP_SYSTEM_TYPE.SET_ADMIN: //群设置管理员(被设置者接收)
        case GROUP_SYSTEM_TYPE.CANCEL_ADMIN: //取消管理员(被取消者接收)
        case GROUP_SYSTEM_TYPE.REVOKE: //群已被回收(全员接收, 不展示)
          break
        case GROUP_SYSTEM_TYPE.CUSTOM: //用户自定义通知(默认全员接收)
          notify['MsgSeq'] = groupTip.MsgSeq
          notify['UserDefinedField'] = groupReportTypeMsg.UserDefinedField
          break
        default:
          log.error('未知群系统消息类型：reportType=' + reportType)
          break
      }
      //回调
      if (onGroupSystemNotifyCallbacks[reportType]) onGroupSystemNotifyCallbacks[reportType](notify)
    }

    //处理C2C EVENT 消息通道Array
    var handlerC2cNotifyMsgArray = function(arr) {
      for (var i = 0, l = arr.length; i < l; i++) {
        handlerC2cEventMsg(arr[i])
      }
    }

    //处理C2C EVENT 消息通道Item
    var handlerC2cEventMsg = function(notify) {
      var subType = notify.SubMsgType
      switch (subType) {
        case C2C_EVENT_SUB_TYPE.READED: //已读通知
          break
        case C2C_EVENT_SUB_TYPE.KICKEDOUT: //已读通知
          if (onC2cEventCallbacks[subType]) {
            onC2cEventCallbacks[subType]()
          }
          break
        default:
          log.error('未知C2c系统消息：subType=' + subType)
          break
      }
      // stopPolling = true;
      //回调onMsgReadCallback
      if (notify.ReadC2cMsgNotify.UinPairReadArray && onC2cEventCallbacks[subType]) {
        for (var i = 0, l = notify.ReadC2cMsgNotify.UinPairReadArray.length; i < l; i++) {
          var item = notify.ReadC2cMsgNotify.UinPairReadArray[i]
          onC2cEventCallbacks[subType](item)
        }
      }
    }

    //长轮询
    this.longPolling = function(cbOk, cbErr) {
      var opts = {
        Timeout: longPollingDefaultTimeOut / 1000,
        Cookie: {
          NotifySeq: notifySeq,
          NoticeSeq: noticeSeq
        }
      }
      if (LongPollingId) {
        opts.Cookie.LongPollingId = LongPollingId
        doPolling()
      } else {
        proto_getLongPollingId({}, function(resp) {
          LongPollingId = opts.Cookie.LongPollingId = resp.LongPollingId
          //根据回包设置超时时间，超时时长不能>60秒，因为webkit手机端的最长超时时间不能大于60s
          longPollingDefaultTimeOut = resp.Timeout > 60 ? longPollingDefaultTimeOut : resp.Timeout * 1000
          doPolling()
        })
      }

      function doPolling() {
        proto_longPolling(
          opts,
          function(resp) {
            for (var i in resp.EventArray) {
              var e = resp.EventArray[i]
              switch (e.Event) {
                case LONG_POLLINNG_EVENT_TYPE.C2C: //c2c消息通知
                  //更新C2C消息通知seq
                  notifySeq = e.NotifySeq
                  log.warn('longpolling: received new c2c msg')
                  //获取新消息
                  MsgManager.syncMsgs()
                  break
                case LONG_POLLINNG_EVENT_TYPE.GROUP_COMMON: //普通群消息通知
                  log.warn('longpolling: received new group msgs')
                  handlerOrdinaryAndTipGroupMsgs(e.Event, e.GroupMsgArray)
                  break
                case LONG_POLLINNG_EVENT_TYPE.GROUP_TIP: //（全员广播）群提示消息
                  log.warn('longpolling: received new group tips')
                  handlerOrdinaryAndTipGroupMsgs(e.Event, e.GroupTips)
                  break
                case LONG_POLLINNG_EVENT_TYPE.GROUP_TIP2: //群提示消息
                  log.warn('longpolling: received new group tips')
                  handlerOrdinaryAndTipGroupMsgs(e.Event, e.GroupTips)
                  break
                case LONG_POLLINNG_EVENT_TYPE.GROUP_SYSTEM: //（多终端同步）群系统消息
                  log.warn('longpolling: received new group system msgs')
                  //false 表示 通过长轮询收到的群系统消息，可以不判重
                  handlerGroupSystemMsgs(e.GroupTips, false)
                  break
                case LONG_POLLINNG_EVENT_TYPE.FRIEND_NOTICE: //好友系统通知
                  log.warn('longpolling: received new friend system notice')
                  //false 表示 通过长轮询收到的好友系统通知，可以不判重
                  handlerFriendSystemNotices(e.FriendListMod, false)
                  break
                case LONG_POLLINNG_EVENT_TYPE.PROFILE_NOTICE: //资料系统通知
                  log.warn('longpolling: received new profile system notice')
                  //false 表示 通过长轮询收到的资料系统通知，可以不判重
                  handlerProfileSystemNotices(e.ProfileDataMod, false)
                  break
                case LONG_POLLINNG_EVENT_TYPE.C2C_COMMON: //c2c消息通知
                  noticeSeq = e.C2cMsgArray[0].NoticeSeq
                  //更新C2C消息通知seq
                  log.warn('longpolling: received new c2c_common msg', noticeSeq)
                  handlerOrdinaryAndTipC2cMsgs(e.Event, e.C2cMsgArray)
                  break
                case LONG_POLLINNG_EVENT_TYPE.C2C_EVENT: //c2c已读消息通知
                  noticeSeq = e.C2cNotifyMsgArray[0].NoticeSeq
                  log.warn('longpolling: received new c2c_event msg')
                  handlerC2cNotifyMsgArray(e.C2cNotifyMsgArray)
                  break
                default:
                  log.error('longpolling收到未知新消息类型: Event=' + e.Event)
                  break
              }
            }
            var successInfo = {
              ActionStatus: ACTION_STATUS.OK,
              ErrorCode: 0
            }
            updatecLongPollingStatus(successInfo)
          },
          function(err) {
            //log.error(err);
            updatecLongPollingStatus(err)
            if (cbErr) cbErr(err)
          }
        )
      }
    }

    //大群 长轮询
    this.bigGroupLongPolling = function(cbOk, cbErr) {
      var GroupId = BigGroupId
      var opts = {
        USP: 1,
        StartSeq: bigGroupLongPollingStartSeq, //请求拉消息的起始seq
        HoldTime: bigGroupLongPollingHoldTime, //客户端长轮询的超时时间，单位是秒
        Key: bigGroupLongPollingKey //客户端加入群组后收到的的Key
      }

      proto_bigGroupLongPolling(
        opts,
        function(resp) {
          if (GroupId != BigGroupId) return

          var msgObjList = []
          bigGroupLongPollingStartSeq = resp.NextSeq
          bigGroupLongPollingHoldTime = resp.HoldTime
          bigGroupLongPollingKey = resp.Key

          if (resp.RspMsgList && resp.RspMsgList.length > 0) {
            var msgCount = 0,
              msgInfo,
              event,
              msg
            for (var i = resp.RspMsgList.length - 1; i >= 0; i--) {
              msgInfo = resp.RspMsgList[i]
              //后台这里做了调整，缩短字段名，以下是兼容代码
              var keyMap = {
                F_Account: 'From_Account',
                T_Account: 'To_Account',
                FAType: 'EnumFrom_AccountType',
                TAType: 'EnumTo_AccountType',
                GCode: 'GroupCode',
                GName: 'GroupName',
                GId: 'GroupId',
                MFlg: 'MsgFlag',
                FAEInfo: 'MsgFrom_AccountExtraInfo',
                Evt: 'Event',
                GInfo: 'GroupInfo',
                BPlc: 'IsPlaceMsg',
                MBody: 'MsgBody',
                Pri: 'MsgPriority',
                Rdm: 'MsgRandom',
                MSeq: 'MsgSeq',
                TStp: 'MsgTimeStamp',
                TGId: 'ToGroupId',
                UEInfo: 'UinExtInfo',
                UId: 'UserId',
                BSys: 'IsSystemMsg',
                FAHUrl: 'From_AccountHeadurl',
                FANick: 'From_AccountNick'
              }
              msgInfo = tool.replaceObject(keyMap, msgInfo)
              //如果是已经删除的消息或者发送者帐号为空或者消息内容为空
              //IsPlaceMsg=1
              if (msgInfo.IsPlaceMsg || !msgInfo.From_Account || !msgInfo.MsgBody || msgInfo.MsgBody.length == 0) {
                continue
              }

              event = msgInfo.Event //群消息类型
              switch (event) {
                case LONG_POLLINNG_EVENT_TYPE.GROUP_COMMON: //群普通消息
                  log.info('bigGroupLongPolling: return new group msg')
                  msg = handlerGroupMsg(msgInfo, false, false)
                  msg && msgObjList.push(msg)
                  msgCount = msgCount + 1
                  break
                case LONG_POLLINNG_EVENT_TYPE.GROUP_TIP: //群提示消息
                case LONG_POLLINNG_EVENT_TYPE.GROUP_TIP2: //群提示消息
                  log.info('bigGroupLongPolling: return new group tip')
                  msg = handlerGroupMsg(msgInfo, false, false)
                  msg && msgObjList.push(msg)
                  //msgCount=msgCount+1;
                  break
                case LONG_POLLINNG_EVENT_TYPE.GROUP_SYSTEM: //群系统消息
                  log.info('bigGroupLongPolling: new group system msg')
                  handlerGroupSystemMsg(msgInfo)
                  break
                default:
                  log.error('bigGroupLongPolling收到未知新消息类型: Event=' + event)
                  break
              }
            } // for loop
            if (msgCount > 0) {
              MsgManager.setBigGroupLongPollingMsgMap(msgInfo.ToGroupId, msgCount) //
              log.warn('current bigGroupLongPollingMsgMap: ' + JSON.stringify(bigGroupLongPollingMsgMap))
            }
          }
          curBigGroupLongPollingRetErrorCount = 0
          //返回连接状态
          var successInfo = {
            ActionStatus: ACTION_STATUS.OK,
            ErrorCode: CONNECTION_STATUS.ON,
            ErrorInfo: 'connection is ok...'
          }
          ConnManager.callBack(successInfo)

          if (cbOk) cbOk(msgObjList)
          else if (onBigGroupMsgCallback) onBigGroupMsgCallback(msgObjList) //返回新消息

          //重新启动长轮询
          bigGroupLongPollingOn && MsgManager.bigGroupLongPolling()
        },
        function(err) {
          //
          if (err.ErrorCode != longPollingTimeOutErrorCode) {
            log.error(err.ErrorInfo)
            //记录长轮询返回错误次数
            curBigGroupLongPollingRetErrorCount++
          }
          if (err.ErrorCode == longPollingKickedErrorCode) {
            //登出
            log.error('多实例登录，被kick')
            if (onKickedEventCall) {
              onKickedEventCall()
            }
          }
          //累计超过一定次数，不再发起长轮询请求
          if (curBigGroupLongPollingRetErrorCount < LONG_POLLING_MAX_RET_ERROR_COUNT) {
            bigGroupLongPollingOn && MsgManager.bigGroupLongPolling()
          } else {
            var errInfo = {
              ActionStatus: ACTION_STATUS.FAIL,
              ErrorCode: CONNECTION_STATUS.OFF,
              ErrorInfo: 'connection is off'
            }
            ConnManager.callBack(errInfo)
          }
          if (cbErr) cbErr(err)
        },
        bigGroupLongPollingHoldTime * 1000
      )
    }

    //更新连接状态
    var updatecLongPollingStatus = function(errObj) {
      if (errObj.ErrorCode == 0 || errObj.ErrorCode == longPollingTimeOutErrorCode) {
        curLongPollingRetErrorCount = 0
        longPollingOffCallbackFlag = false
        var errorInfo
        var isNeedCallback = false
        switch (curLongPollingStatus) {
          case CONNECTION_STATUS.INIT:
            isNeedCallback = true
            curLongPollingStatus = CONNECTION_STATUS.ON
            errorInfo = 'create connection successfully(INIT->ON)'
            break
          case CONNECTION_STATUS.ON:
            errorInfo = 'connection is on...(ON->ON)'
            break
          case CONNECTION_STATUS.RECONNECT:
            curLongPollingStatus = CONNECTION_STATUS.ON
            errorInfo = 'connection is on...(RECONNECT->ON)'
            break
          case CONNECTION_STATUS.OFF:
            isNeedCallback = true
            curLongPollingStatus = CONNECTION_STATUS.RECONNECT
            errorInfo = 'reconnect successfully(OFF->RECONNECT)'
            break
        }
        var successInfo = {
          ActionStatus: ACTION_STATUS.OK,
          ErrorCode: curLongPollingStatus,
          ErrorInfo: errorInfo
        }
        isNeedCallback && ConnManager.callBack(successInfo)
        longPollingOn && MsgManager.longPolling()
      } else if (errObj.ErrorCode == longPollingKickedErrorCode) {
        //登出
        log.error('多实例登录，被kick')
        if (onKickedEventCall) {
          onKickedEventCall()
        }
      } else {
        //记录长轮询返回解析json错误次数
        curLongPollingRetErrorCount++
        log.warn('longPolling接口第' + curLongPollingRetErrorCount + '次报错: ' + errObj.ErrorInfo)
        //累计超过一定次数
        if (curLongPollingRetErrorCount <= LONG_POLLING_MAX_RET_ERROR_COUNT) {
          setTimeout(startNextLongPolling, 100) //
        } else {
          curLongPollingStatus = CONNECTION_STATUS.OFF
          var errInfo = {
            ActionStatus: ACTION_STATUS.FAIL,
            ErrorCode: CONNECTION_STATUS.OFF,
            ErrorInfo: 'connection is off'
          }
          longPollingOffCallbackFlag == false && ConnManager.callBack(errInfo)
          longPollingOffCallbackFlag = true
          log.warn(longPollingIntervalTime + '毫秒之后,SDK会发起新的longPolling请求...')
          setTimeout(startNextLongPolling, longPollingIntervalTime) //长轮询接口报错次数达到一定值，每间隔5s发起新的长轮询
        }
      }
    }

    //处理收到的普通C2C消息
    var handlerOrdinaryAndTipC2cMsgs = function(eventType, C2cMsgArray) {
      //处理c2c消息
      var notifyInfo = []
      var msgInfos = []
      msgInfos = C2cMsgArray //返回的消息列表
      // MsgStore.cookie = resp.Cookie;//cookies，记录当前读到的最新消息位置

      for (var i in msgInfos) {
        var msgInfo = msgInfos[i]
        var isSendMsg, id, headUrl
        if (msgInfo.From_Account == ctx.identifier) {
          //当前用户发送的消息
          isSendMsg = true
          id = msgInfo.To_Account //读取接收者信息
          headUrl = ''
        } else {
          //当前用户收到的消息
          isSendMsg = false
          id = msgInfo.From_Account //读取发送者信息
          headUrl = ''
        }
        var sess = MsgStore.sessByTypeId(SESSION_TYPE.C2C, id)
        if (!sess) {
          sess = new Session(SESSION_TYPE.C2C, id, id, headUrl, 0, 0)
        }
        var msg = new Msg(
          sess,
          isSendMsg,
          msgInfo.MsgSeq,
          msgInfo.MsgRandom,
          msgInfo.MsgTimeStamp,
          msgInfo.From_Account
        )
        var msgBody = null
        var msgContent = null
        var msgType = null
        for (var mi in msgInfo.MsgBody) {
          msgBody = msgInfo.MsgBody[mi]
          msgType = msgBody.MsgType
          switch (msgType) {
            case MSG_ELEMENT_TYPE.TEXT:
              msgContent = new Msg.Elem.Text(msgBody.MsgContent.Text)
              break
            case MSG_ELEMENT_TYPE.FACE:
              msgContent = new Msg.Elem.Face(msgBody.MsgContent.Index, msgBody.MsgContent.Data)
              break
            case MSG_ELEMENT_TYPE.IMAGE:
              msgContent = new Msg.Elem.Images(msgBody.MsgContent.UUID, msgBody.MsgContent.ImageFormat || '')
              for (var j in msgBody.MsgContent.ImageInfoArray) {
                var tempImg = msgBody.MsgContent.ImageInfoArray[j]
                msgContent.addImage(
                  new Msg.Elem.Images.Image(tempImg.Type, tempImg.Size, tempImg.Width, tempImg.Height, tempImg.URL)
                )
              }
              break
            case MSG_ELEMENT_TYPE.SOUND:
              if (msgBody.MsgContent) {
                msgContent = new Msg.Elem.Sound(
                  msgBody.MsgContent.UUID,
                  msgBody.MsgContent.Second,
                  msgBody.MsgContent.Size,
                  msgInfo.From_Account,
                  msgInfo.To_Account,
                  msgBody.MsgContent.Download_Flag,
                  SESSION_TYPE.C2C
                )
              } else {
                msgType = MSG_ELEMENT_TYPE.TEXT
                msgContent = new Msg.Elem.Text('[语音消息]下载地址解析出错')
              }
              break
            case MSG_ELEMENT_TYPE.LOCATION:
              msgContent = new Msg.Elem.Location(
                msgBody.MsgContent.Longitude,
                msgBody.MsgContent.Latitude,
                msgBody.MsgContent.Desc
              )
              break
            case MSG_ELEMENT_TYPE.FILE:
            case MSG_ELEMENT_TYPE.FILE + ' ':
              msgType = MSG_ELEMENT_TYPE.FILE
              if (msgBody.MsgContent) {
                msgContent = new Msg.Elem.File(
                  msgBody.MsgContent.UUID,
                  msgBody.MsgContent.FileName,
                  msgBody.MsgContent.FileSize,
                  msgInfo.From_Account,
                  msgInfo.To_Account,
                  msgBody.MsgContent.Download_Flag,
                  SESSION_TYPE.C2C
                )
              } else {
                msgType = MSG_ELEMENT_TYPE.TEXT
                msgContent = new Msg.Elem.Text('[文件消息下载地址解析出错]')
              }
              break
            case MSG_ELEMENT_TYPE.CUSTOM:
              try {
                var data = JSON.parse(msgBody.MsgContent.Data)
                if (data && data.userAction && data.userAction == FRIEND_WRITE_MSG_ACTION.ING) {
                  //过滤安卓或ios的正在输入自定义消息
                  continue
                }
              } catch (e) {}

              msgType = MSG_ELEMENT_TYPE.CUSTOM
              msgContent = new Msg.Elem.Custom(msgBody.MsgContent.Data, msgBody.MsgContent.Desc, msgBody.MsgContent.Ext)
              break
            default:
              msgType = MSG_ELEMENT_TYPE.TEXT
              msgContent = new Msg.Elem.Text('web端暂不支持' + msgBody.MsgType + '消息')
              break
          }
          msg.elems.push(new Msg.Elem(msgType, msgContent))
        }

        if (msg.elems.length > 0 && MsgStore.addMsg(msg)) {
          notifyInfo.push(msg)
        }
      } // for loop
      if (notifyInfo.length > 0) MsgStore.updateTimeline()
      if (notifyInfo.length > 0) {
        if (onMsgCallback) onMsgCallback(notifyInfo)
      }
    }

    //发起新的长轮询请求
    var startNextLongPolling = function() {
      longPollingOn && MsgManager.longPolling()
    }

    //处理未决的加群申请消息列表
    var handlerApplyJoinGroupSystemMsgs = function(eventArray) {
      for (var i in eventArray) {
        var e = eventArray[i]
        handlerGroupSystemMsgs(e.GroupTips, true)
        switch (e.Event) {
          case LONG_POLLINNG_EVENT_TYPE.GROUP_SYSTEM: //（多终端同步）群系统消息
            log.warn('handlerApplyJoinGroupSystemMsgs： handler new group system msg')
            //true 表示 解决加群申请通知存在重复的问题（已处理的通知，下次登录还会拉到），需要判重
            handlerGroupSystemMsgs(e.GroupTips, true)
            break
          default:
            log.error('syncMsgs收到未知的群系统消息类型: Event=' + e.Event)
            break
        }
      }
    }

    //拉取c2c消息(包含加群未决消息，需要处理)
    this.syncMsgs = function(cbOk, cbErr) {
      var notifyInfo = []
      var msgInfos = []
      //读取C2C消息
      proto_getMsgs(
        MsgStore.cookie,
        MsgStore.syncFlag,
        function(resp) {
          //拉取完毕
          if (resp.SyncFlag == 2) {
            MsgStore.syncFlag = 0
          }
          //处理c2c消息
          msgInfos = resp.MsgList //返回的消息列表
          MsgStore.cookie = resp.Cookie //cookies，记录当前读到的最新消息位置

          for (var i in msgInfos) {
            var msgInfo = msgInfos[i]
            var isSendMsg, id, headUrl
            if (msgInfo.From_Account == ctx.identifier) {
              //当前用户发送的消息
              isSendMsg = true
              id = msgInfo.To_Account //读取接收者信息
              headUrl = ''
            } else {
              //当前用户收到的消息
              isSendMsg = false
              id = msgInfo.From_Account //读取发送者信息
              headUrl = ''
            }
            var sess = MsgStore.sessByTypeId(SESSION_TYPE.C2C, id)
            if (!sess) {
              sess = new Session(SESSION_TYPE.C2C, id, id, headUrl, 0, 0)
            }
            var msg = new Msg(
              sess,
              isSendMsg,
              msgInfo.MsgSeq,
              msgInfo.MsgRandom,
              msgInfo.MsgTimeStamp,
              msgInfo.From_Account
            )
            var msgBody = null
            var msgContent = null
            var msgType = null
            for (var mi in msgInfo.MsgBody) {
              msgBody = msgInfo.MsgBody[mi]
              msgType = msgBody.MsgType
              switch (msgType) {
                case MSG_ELEMENT_TYPE.TEXT:
                  msgContent = new Msg.Elem.Text(msgBody.MsgContent.Text)
                  break
                case MSG_ELEMENT_TYPE.FACE:
                  msgContent = new Msg.Elem.Face(msgBody.MsgContent.Index, msgBody.MsgContent.Data)
                  break
                case MSG_ELEMENT_TYPE.IMAGE:
                  msgContent = new Msg.Elem.Images(msgBody.MsgContent.UUID, msgBody.MsgContent.ImageFormat)
                  for (var j in msgBody.MsgContent.ImageInfoArray) {
                    var tempImg = msgBody.MsgContent.ImageInfoArray[j]
                    msgContent.addImage(
                      new Msg.Elem.Images.Image(tempImg.Type, tempImg.Size, tempImg.Width, tempImg.Height, tempImg.URL)
                    )
                  }
                  break
                case MSG_ELEMENT_TYPE.SOUND:
                  // var soundUrl = getSoundDownUrl(msgBody.MsgContent.UUID, msgInfo.From_Account);
                  if (msgBody.MsgContent) {
                    msgContent = new Msg.Elem.Sound(
                      msgBody.MsgContent.UUID,
                      msgBody.MsgContent.Second,
                      msgBody.MsgContent.Size,
                      msgInfo.From_Account,
                      msgInfo.To_Account,
                      msgBody.MsgContent.Download_Flag,
                      SESSION_TYPE.C2C
                    )
                  } else {
                    msgType = MSG_ELEMENT_TYPE.TEXT
                    msgContent = new Msg.Elem.Text('[语音消息]下载地址解析出错')
                  }
                  break
                case MSG_ELEMENT_TYPE.LOCATION:
                  msgContent = new Msg.Elem.Location(
                    msgBody.MsgContent.Longitude,
                    msgBody.MsgContent.Latitude,
                    msgBody.MsgContent.Desc
                  )
                  break
                case MSG_ELEMENT_TYPE.FILE:
                case MSG_ELEMENT_TYPE.FILE + ' ':
                  msgType = MSG_ELEMENT_TYPE.FILE
                  // var fileUrl = getFileDownUrl(msgBody.MsgContent.UUID, msgInfo.From_Account, msgBody.MsgContent.FileName);
                  if (msgBody.MsgContent) {
                    msgContent = new Msg.Elem.File(
                      msgBody.MsgContent.UUID,
                      msgBody.MsgContent.FileName,
                      msgBody.MsgContent.FileSize,
                      msgInfo.From_Account,
                      msgInfo.To_Account,
                      msgBody.MsgContent.Download_Flag,
                      SESSION_TYPE.C2C
                    )
                  } else {
                    msgType = MSG_ELEMENT_TYPE.TEXT
                    msgContent = new Msg.Elem.Text('[文件消息下载地址解析出错]')
                  }
                  break
                case MSG_ELEMENT_TYPE.CUSTOM:
                  try {
                    var data = JSON.parse(msgBody.MsgContent.Data)
                    if (data && data.userAction && data.userAction == FRIEND_WRITE_MSG_ACTION.ING) {
                      //过滤安卓或ios的正在输入自定义消息
                      continue
                    }
                  } catch (e) {}

                  msgType = MSG_ELEMENT_TYPE.CUSTOM
                  msgContent = new Msg.Elem.Custom(
                    msgBody.MsgContent.Data,
                    msgBody.MsgContent.Desc,
                    msgBody.MsgContent.Ext
                  )
                  break
                default:
                  msgType = MSG_ELEMENT_TYPE.TEXT
                  msgContent = new Msg.Elem.Text('web端暂不支持' + msgBody.MsgType + '消息')
                  break
              }
              msg.elems.push(new Msg.Elem(msgType, msgContent))
            }

            if (msg.elems.length > 0 && MsgStore.addMsg(msg)) {
              notifyInfo.push(msg)
            }
          } // for loop

          //处理加群未决申请消息
          handlerApplyJoinGroupSystemMsgs(resp.EventArray)

          if (notifyInfo.length > 0) MsgStore.updateTimeline()
          if (cbOk) cbOk(notifyInfo)
          else if (notifyInfo.length > 0) {
            if (onMsgCallback) onMsgCallback(notifyInfo)
          }
        },
        function(err) {
          log.error('getMsgs failed:' + err.ErrorInfo)
          if (cbErr) cbErr(err)
        }
      )
    }

    //拉取C2C漫游消息
    this.getC2CHistoryMsgs = function(options, cbOk, cbErr) {
      if (!options.Peer_Account) {
        if (cbErr) {
          cbErr(tool.getReturnError('Peer_Account is empty', -13))
          return
        }
      }
      if (!options.MaxCnt) {
        options.MaxCnt = 15
      }
      if (options.MaxCnt <= 0) {
        if (cbErr) {
          cbErr(tool.getReturnError('MaxCnt should be greater than 0', -14))
          return
        }
      }
      if (options.MaxCnt > 15) {
        if (cbErr) {
          cbErr(tool.getReturnError('MaxCnt can not be greater than 15', -15))
          return
        }
        return
      }
      if (options.MsgKey == null || options.MsgKey === undefined) {
        options.MsgKey = ''
      }
      var opts = {
        Peer_Account: options.Peer_Account,
        MaxCnt: options.MaxCnt,
        LastMsgTime: options.LastMsgTime,
        MsgKey: options.MsgKey
      }
      //读取c2c漫游消息
      proto_getC2CHistoryMsgs(
        opts,
        function(resp) {
          var msgObjList = []
          var msgInfos = []
          //处理c2c消息
          msgInfos = resp.MsgList //返回的消息列表
          var sess = MsgStore.sessByTypeId(SESSION_TYPE.C2C, options.Peer_Account)
          if (!sess) {
            sess = new Session(SESSION_TYPE.C2C, options.Peer_Account, options.Peer_Account, '', 0, 0)
          }
          for (var i in msgInfos) {
            var msgInfo = msgInfos[i]
            var isSendMsg, id, headUrl
            if (msgInfo.From_Account == ctx.identifier) {
              //当前用户发送的消息
              isSendMsg = true
              id = msgInfo.To_Account //读取接收者信息
              headUrl = ''
            } else {
              //当前用户收到的消息
              isSendMsg = false
              id = msgInfo.From_Account //读取发送者信息
              headUrl = ''
            }
            var msg = new Msg(
              sess,
              isSendMsg,
              msgInfo.MsgSeq,
              msgInfo.MsgRandom,
              msgInfo.MsgTimeStamp,
              msgInfo.From_Account
            )
            var msgBody = null
            var msgContent = null
            var msgType = null
            for (var mi in msgInfo.MsgBody) {
              msgBody = msgInfo.MsgBody[mi]
              msgType = msgBody.MsgType
              switch (msgType) {
                case MSG_ELEMENT_TYPE.TEXT:
                  msgContent = new Msg.Elem.Text(msgBody.MsgContent.Text)
                  break
                case MSG_ELEMENT_TYPE.FACE:
                  msgContent = new Msg.Elem.Face(msgBody.MsgContent.Index, msgBody.MsgContent.Data)
                  break
                case MSG_ELEMENT_TYPE.IMAGE:
                  msgContent = new Msg.Elem.Images(msgBody.MsgContent.UUID, msgBody.MsgContent.ImageFormat)
                  for (var j in msgBody.MsgContent.ImageInfoArray) {
                    var tempImg = msgBody.MsgContent.ImageInfoArray[j]
                    msgContent.addImage(
                      new Msg.Elem.Images.Image(tempImg.Type, tempImg.Size, tempImg.Width, tempImg.Height, tempImg.URL)
                    )
                  }
                  break
                case MSG_ELEMENT_TYPE.SOUND:
                  // var soundUrl = getSoundDownUrl(msgBody.MsgContent.UUID, msgInfo.From_Account);

                  if (msgBody.MsgContent) {
                    msgContent = new Msg.Elem.Sound(
                      msgBody.MsgContent.UUID,
                      msgBody.MsgContent.Second,
                      msgBody.MsgContent.Size,
                      msgInfo.From_Account,
                      msgInfo.To_Account,
                      msgBody.MsgContent.Download_Flag,
                      SESSION_TYPE.C2C
                    )
                  } else {
                    msgType = MSG_ELEMENT_TYPE.TEXT
                    msgContent = new Msg.Elem.Text('[语音消息]下载地址解析出错')
                  }
                  break
                case MSG_ELEMENT_TYPE.LOCATION:
                  msgContent = new Msg.Elem.Location(
                    msgBody.MsgContent.Longitude,
                    msgBody.MsgContent.Latitude,
                    msgBody.MsgContent.Desc
                  )
                  break
                case MSG_ELEMENT_TYPE.FILE:
                case MSG_ELEMENT_TYPE.FILE + ' ':
                  msgType = MSG_ELEMENT_TYPE.FILE
                  // var fileUrl = getFileDownUrl(msgBody.MsgContent.UUID, msgInfo.From_Account, msgBody.MsgContent.FileName);

                  if (msgBody.MsgContent) {
                    msgContent = new Msg.Elem.File(
                      msgBody.MsgContent.UUID,
                      msgBody.MsgContent.FileName,
                      msgBody.MsgContent.FileSize,
                      msgInfo.From_Account,
                      msgInfo.To_Account,
                      msgBody.MsgContent.Download_Flag,
                      SESSION_TYPE.C2C
                    )
                  } else {
                    msgType = MSG_ELEMENT_TYPE.TEXT
                    msgContent = new Msg.Elem.Text('[文件消息下载地址解析出错]')
                  }
                  break
                case MSG_ELEMENT_TYPE.CUSTOM:
                  msgType = MSG_ELEMENT_TYPE.CUSTOM
                  msgContent = new Msg.Elem.Custom(
                    msgBody.MsgContent.Data,
                    msgBody.MsgContent.Desc,
                    msgBody.MsgContent.Ext
                  )

                  break
                default:
                  msgType = MSG_ELEMENT_TYPE.TEXT
                  msgContent = new Msg.Elem.Text('web端暂不支持' + msgBody.MsgType + '消息')
                  break
              }
              msg.elems.push(new Msg.Elem(msgType, msgContent))
            }
            MsgStore.addMsg(msg)
            msgObjList.push(msg)
          } // for loop

          MsgStore.updateTimeline()
          if (cbOk) {
            var newResp = {
              Complete: resp.Complete,
              MsgCount: msgObjList.length,
              LastMsgTime: resp.LastMsgTime,
              MsgKey: resp.MsgKey,
              MsgList: msgObjList
            }
            sess.isFinished(resp.Complete)
            cbOk(newResp)
          }
        },
        function(err) {
          log.error('getC2CHistoryMsgs failed:' + err.ErrorInfo)
          if (cbErr) cbErr(err)
        }
      )
    }

    //拉群历史消息
    //不传cbOk 和 cbErr，则会调用新消息回调函数
    this.syncGroupMsgs = function(options, cbOk, cbErr) {
      if (options.ReqMsgSeq <= 0) {
        if (cbErr) {
          var errInfo = 'ReqMsgSeq must be greater than 0'
          var error = tool.getReturnError(errInfo, -16)
          cbErr(error)
        }
        return
      }
      var opts = {
        GroupId: options.GroupId,
        ReqMsgSeq: options.ReqMsgSeq,
        ReqMsgNumber: options.ReqMsgNumber
      }
      //读群漫游消息
      proto_getGroupMsgs(
        opts,
        function(resp) {
          var notifyInfo = []
          var group_id = resp.GroupId //返回的群id
          var msgInfos = resp.RspMsgList //返回的消息列表
          var isFinished = resp.IsFinished

          if (msgInfos == null || msgInfos === undefined) {
            if (cbOk) {
              cbOk([])
            }
            return
          }
          for (var i = msgInfos.length - 1; i >= 0; i--) {
            var msgInfo = msgInfos[i]
            //如果是已经删除的消息或者发送者帐号为空或者消息内容为空
            //IsPlaceMsg=1
            if (msgInfo.IsPlaceMsg || !msgInfo.From_Account || !msgInfo.MsgBody || msgInfo.MsgBody.length == 0) {
              continue
            }
            var msg = handlerGroupMsg(msgInfo, true, true, isFinished)
            if (msg) {
              notifyInfo.push(msg)
            }
          } // for loop
          if (notifyInfo.length > 0) MsgStore.updateTimeline()
          if (cbOk) cbOk(notifyInfo)
          else if (notifyInfo.length > 0) {
            if (onMsgCallback) onMsgCallback(notifyInfo)
          }
        },
        function(err) {
          log.error('getGroupMsgs failed:' + err.ErrorInfo)
          if (cbErr) cbErr(err)
        }
      )
    }

    //处理群消息(普通消息+提示消息)
    //isSyncGroupMsgs 是否主动拉取群消息标志
    //isAddMsgFlag 是否需要保存到MsgStore，如果需要，这里会存在判重逻辑
    var handlerGroupMsg = function(msgInfo, isSyncGroupMsgs, isAddMsgFlag, isFinished) {
      if (msgInfo.IsPlaceMsg || !msgInfo.From_Account || !msgInfo.MsgBody || msgInfo.MsgBody.length == 0) {
        return null
      }
      var isSendMsg, id, headUrl, fromAccountNick, fromAccountHeadurl
      var group_id = msgInfo.ToGroupId
      var group_name = group_id
      if (msgInfo.GroupInfo) {
        //取出群名称
        if (msgInfo.GroupInfo.GroupName) {
          group_name = msgInfo.GroupInfo.GroupName
        }
      }
      //取出成员昵称
      fromAccountNick = msgInfo.From_Account
      //fromAccountHeadurl = msgInfo.GroupInfo.From_AccountHeadurl;
      if (msgInfo.GroupInfo) {
        if (msgInfo.GroupInfo.From_AccountNick) {
          fromAccountNick = msgInfo.GroupInfo.From_AccountNick
        }
        if (msgInfo.GroupInfo.From_AccountHeadurl) {
          fromAccountHeadurl = msgInfo.GroupInfo.From_AccountHeadurl
        } else {
          fromAccountHeadurl = null
        }
      }
      if (msgInfo.From_Account == ctx.identifier) {
        //当前用户发送的消息
        isSendMsg = true
        id = msgInfo.From_Account //读取接收者信息
        headUrl = ''
      } else {
        //当前用户收到的消息
        isSendMsg = false
        id = msgInfo.From_Account //读取发送者信息
        headUrl = ''
      }
      var sess = MsgStore.sessByTypeId(SESSION_TYPE.GROUP, group_id)
      if (!sess) {
        sess = new Session(SESSION_TYPE.GROUP, group_id, group_name, headUrl, 0, 0)
      }
      if (typeof isFinished !== 'undefined') {
        sess.isFinished(isFinished || 0)
      }
      var subType = GROUP_MSG_SUB_TYPE.COMMON //消息类型
      //群提示消息,重新封装下
      if (LONG_POLLINNG_EVENT_TYPE.GROUP_TIP == msgInfo.Event || LONG_POLLINNG_EVENT_TYPE.GROUP_TIP2 == msgInfo.Event) {
        subType = GROUP_MSG_SUB_TYPE.TIP
        var groupTip = msgInfo.MsgBody
        msgInfo.MsgBody = []
        msgInfo.MsgBody.push({
          MsgType: MSG_ELEMENT_TYPE.GROUP_TIP,
          MsgContent: groupTip
        })
      } else if (msgInfo.MsgPriority) {
        //群点赞消息
        if (msgInfo.MsgPriority == GROUP_MSG_PRIORITY_TYPE.REDPACKET) {
          subType = GROUP_MSG_SUB_TYPE.REDPACKET
        } else if (msgInfo.MsgPriority == GROUP_MSG_PRIORITY_TYPE.LOVEMSG) {
          subType = GROUP_MSG_SUB_TYPE.LOVEMSG
        }
      }
      var msg = new Msg(
        sess,
        isSendMsg,
        msgInfo.MsgSeq,
        msgInfo.MsgRandom,
        msgInfo.MsgTimeStamp,
        msgInfo.From_Account,
        subType,
        fromAccountNick,
        fromAccountHeadurl
      )
      var msgBody = null
      var msgContent = null
      var msgType = null
      for (var mi in msgInfo.MsgBody) {
        msgBody = msgInfo.MsgBody[mi]
        msgType = msgBody.MsgType
        switch (msgType) {
          case MSG_ELEMENT_TYPE.TEXT:
            msgContent = new Msg.Elem.Text(msgBody.MsgContent.Text)
            break
          case MSG_ELEMENT_TYPE.FACE:
            msgContent = new Msg.Elem.Face(msgBody.MsgContent.Index, msgBody.MsgContent.Data)
            break
          case MSG_ELEMENT_TYPE.IMAGE:
            msgContent = new Msg.Elem.Images(msgBody.MsgContent.UUID, msgBody.MsgContent.ImageFormat || '')
            for (var j in msgBody.MsgContent.ImageInfoArray) {
              msgContent.addImage(
                new Msg.Elem.Images.Image(
                  msgBody.MsgContent.ImageInfoArray[j].Type,
                  msgBody.MsgContent.ImageInfoArray[j].Size,
                  msgBody.MsgContent.ImageInfoArray[j].Width,
                  msgBody.MsgContent.ImageInfoArray[j].Height,
                  msgBody.MsgContent.ImageInfoArray[j].URL
                )
              )
            }
            break
          case MSG_ELEMENT_TYPE.SOUND:
            if (msgBody.MsgContent) {
              msgContent = new Msg.Elem.Sound(
                msgBody.MsgContent.UUID,
                msgBody.MsgContent.Second,
                msgBody.MsgContent.Size,
                msgInfo.From_Account,
                msgInfo.To_Account,
                msgBody.MsgContent.Download_Flag,
                SESSION_TYPE.GROUP
              )
            } else {
              msgType = MSG_ELEMENT_TYPE.TEXT
              msgContent = new Msg.Elem.Text('[语音消息]下载地址解析出错')
            }
            break
          case MSG_ELEMENT_TYPE.LOCATION:
            msgContent = new Msg.Elem.Location(
              msgBody.MsgContent.Longitude,
              msgBody.MsgContent.Latitude,
              msgBody.MsgContent.Desc
            )
            break
          case MSG_ELEMENT_TYPE.FILE:
          case MSG_ELEMENT_TYPE.FILE + ' ':
            msgType = MSG_ELEMENT_TYPE.FILE
            var fileUrl = getFileDownUrl(msgBody.MsgContent.UUID, msgInfo.From_Account, msgBody.MsgContent.FileName)

            if (msgBody.MsgContent) {
              msgContent = new Msg.Elem.File(
                msgBody.MsgContent.UUID,
                msgBody.MsgContent.FileName,
                msgBody.MsgContent.FileSize,
                msgInfo.From_Account,
                msgInfo.To_Account,
                msgBody.MsgContent.Download_Flag,
                SESSION_TYPE.GROUP
              )
            } else {
              msgType = MSG_ELEMENT_TYPE.TEXT
              msgContent = new Msg.Elem.Text('[文件消息]地址解析出错')
            }
            break
          case MSG_ELEMENT_TYPE.GROUP_TIP:
            var opType = msgBody.MsgContent.OpType
            msgContent = new Msg.Elem.GroupTip(
              opType,
              msgBody.MsgContent.Operator_Account,
              group_id,
              msgInfo.GroupInfo.GroupName,
              msgBody.MsgContent.List_Account,
              msgBody.MsgContent.MsgMemberExtraInfo
            )
            if (GROUP_TIP_TYPE.JOIN == opType || GROUP_TIP_TYPE.QUIT == opType) {
              //加群或退群时，设置最新群成员数
              msgContent.setGroupMemberNum(msgBody.MsgContent.MemberNum)
            } else if (GROUP_TIP_TYPE.MODIFY_GROUP_INFO == opType) {
              //群资料变更
              var tempIsCallbackFlag = false
              var tempNewGroupInfo = {
                GroupId: group_id,
                GroupFaceUrl: null,
                GroupName: null,
                OwnerAccount: null,
                GroupNotification: null,
                GroupIntroduction: null
              }
              var msgGroupNewInfo = msgBody.MsgContent.MsgGroupNewInfo
              if (msgGroupNewInfo.GroupFaceUrl) {
                var tmpNGIFaceUrl = new Msg.Elem.GroupTip.GroupInfo(
                  GROUP_TIP_MODIFY_GROUP_INFO_TYPE.FACE_URL,
                  msgGroupNewInfo.GroupFaceUrl
                )
                msgContent.addGroupInfo(tmpNGIFaceUrl)
                tempIsCallbackFlag = true
                tempNewGroupInfo.GroupFaceUrl = msgGroupNewInfo.GroupFaceUrl
              }
              if (msgGroupNewInfo.GroupName) {
                var tmpNGIName = new Msg.Elem.GroupTip.GroupInfo(
                  GROUP_TIP_MODIFY_GROUP_INFO_TYPE.NAME,
                  msgGroupNewInfo.GroupName
                )
                msgContent.addGroupInfo(tmpNGIName)
                tempIsCallbackFlag = true
                tempNewGroupInfo.GroupName = msgGroupNewInfo.GroupName
              }
              if (msgGroupNewInfo.Owner_Account) {
                var tmpNGIOwner = new Msg.Elem.GroupTip.GroupInfo(
                  GROUP_TIP_MODIFY_GROUP_INFO_TYPE.OWNER,
                  msgGroupNewInfo.Owner_Account
                )
                msgContent.addGroupInfo(tmpNGIOwner)
                tempIsCallbackFlag = true
                tempNewGroupInfo.OwnerAccount = msgGroupNewInfo.Owner_Account
              }
              if (msgGroupNewInfo.GroupNotification) {
                var tmpNGINotification = new Msg.Elem.GroupTip.GroupInfo(
                  GROUP_TIP_MODIFY_GROUP_INFO_TYPE.NOTIFICATION,
                  msgGroupNewInfo.GroupNotification
                )
                msgContent.addGroupInfo(tmpNGINotification)
                tempIsCallbackFlag = true
                tempNewGroupInfo.GroupNotification = msgGroupNewInfo.GroupNotification
              }
              if (msgGroupNewInfo.GroupIntroduction) {
                var tmpNGIIntroduction = new Msg.Elem.GroupTip.GroupInfo(
                  GROUP_TIP_MODIFY_GROUP_INFO_TYPE.INTRODUCTION,
                  msgGroupNewInfo.GroupIntroduction
                )
                msgContent.addGroupInfo(tmpNGIIntroduction)
                tempIsCallbackFlag = true
                tempNewGroupInfo.GroupIntroduction = msgGroupNewInfo.GroupIntroduction
              }

              //回调群资料变化通知方法
              if (isSyncGroupMsgs == false && tempIsCallbackFlag && onGroupInfoChangeCallback) {
                onGroupInfoChangeCallback(tempNewGroupInfo)
              }
            } else if (GROUP_TIP_TYPE.MODIFY_MEMBER_INFO == opType) {
              //群成员变更
              var memberInfos = msgBody.MsgContent.MsgMemberInfo
              for (var n in memberInfos) {
                var memberInfo = memberInfos[n]
                msgContent.addMemberInfo(
                  new Msg.Elem.GroupTip.MemberInfo(memberInfo.User_Account, memberInfo.ShutupTime)
                )
              }
            }
            break
          case MSG_ELEMENT_TYPE.CUSTOM:
            msgType = MSG_ELEMENT_TYPE.CUSTOM
            msgContent = new Msg.Elem.Custom(msgBody.MsgContent.Data, msgBody.MsgContent.Desc, msgBody.MsgContent.Ext)
            break
          default:
            msgType = MSG_ELEMENT_TYPE.TEXT
            msgContent = new Msg.Elem.Text('web端暂不支持' + msgBody.MsgType + '消息')
            break
        }
        msg.elems.push(new Msg.Elem(msgType, msgContent))
      }

      if (isAddMsgFlag == false) {
        //不需要保存消息
        return msg
      }

      if (MsgStore.addMsg(msg)) {
        msg.extraInfo = msgInfo.GroupInfo.MsgFrom_AccountExtraInfo
        return msg
      } else {
        return null
      }
    }

    //初始化
    this.init = function(listeners, cbOk, cbErr) {
      if (!listeners.onMsgNotify) {
        log.warn('listeners.onMsgNotify is empty')
      }
      onMsgCallback = listeners.onMsgNotify

      if (listeners.onBigGroupMsgNotify) {
        onBigGroupMsgCallback = listeners.onBigGroupMsgNotify
      } else {
        log.warn('listeners.onBigGroupMsgNotify is empty')
      }

      if (listeners.onC2cEventNotifys) {
        onC2cEventCallbacks = listeners.onC2cEventNotifys
      } else {
        log.warn('listeners.onC2cEventNotifys is empty')
      }
      if (listeners.onGroupSystemNotifys) {
        onGroupSystemNotifyCallbacks = listeners.onGroupSystemNotifys
      } else {
        log.warn('listeners.onGroupSystemNotifys is empty')
      }
      if (listeners.onGroupInfoChangeNotify) {
        onGroupInfoChangeCallback = listeners.onGroupInfoChangeNotify
      } else {
        log.warn('listeners.onGroupInfoChangeNotify is empty')
      }
      if (listeners.onFriendSystemNotifys) {
        onFriendSystemNotifyCallbacks = listeners.onFriendSystemNotifys
      } else {
        log.warn('listeners.onFriendSystemNotifys is empty')
      }
      if (listeners.onProfileSystemNotifys) {
        onProfileSystemNotifyCallbacks = listeners.onProfileSystemNotifys
      } else {
        log.warn('listeners.onProfileSystemNotifys is empty')
      }
      if (listeners.onKickedEventCall) {
        onKickedEventCall = listeners.onKickedEventCall
      } else {
        log.warn('listeners.onKickedEventCall is empty')
      }

      if (listeners.onAppliedDownloadUrl) {
        onAppliedDownloadUrl = listeners.onAppliedDownloadUrl
      } else {
        log.warn('listeners.onAppliedDownloadUrl is empty')
      }

      if (!ctx.identifier || !ctx.userSig) {
        if (cbOk) {
          var success = {
            ActionStatus: ACTION_STATUS.OK,
            ErrorCode: 0,
            ErrorInfo: 'login success(no login state)'
          }
          cbOk(success)
        }
        return
      }

      //初始化
      initMyGroupMaxSeqs(function(resp) {
        log.info('initMyGroupMaxSeqs success')
        //初始化文件
        initIpAndAuthkey(function(initIpAndAuthkeyResp) {
          log.info('initIpAndAuthkey success')
          if (cbOk) {
            log.info('login success(have login state))')
            var success = {
              ActionStatus: ACTION_STATUS.OK,
              ErrorCode: 0,
              ErrorInfo: 'login success'
            }
            cbOk(success)
          }
          MsgManager.setLongPollingOn(true) //开启长轮询
          longPollingOn && MsgManager.longPolling(cbOk)
        }, cbErr)
      }, cbErr)
    }

    //发消息（私聊或群聊）
    this.sendMsg = function(msg, cbOk, cbErr) {
      proto_sendMsg(
        msg,
        function(resp) {
          //私聊时，加入自己的发的消息，群聊时，由于seq和服务器的seq不一样，所以不作处理
          if (msg.sess.type() == SESSION_TYPE.C2C) {
            if (!MsgStore.addMsg(msg)) {
              var errInfo = 'sendMsg: addMsg failed!'
              var error = tool.getReturnError(errInfo, -17)
              log.error(errInfo)
              if (cbErr) cbErr(error)
              return
            }
            //更新信息流时间
            MsgStore.updateTimeline()
          }
          if (cbOk) cbOk(resp)
        },
        function(err) {
          if (cbErr) cbErr(err)
        }
      )
    }
  }()

  //上传文件
  var FileUploader = new function() {
    this.fileMd5 = null
    //获取文件MD5
    var getFileMD5 = function(file, cbOk, cbErr) {
      //FileReader pc浏览器兼容性
      //Feature   Firefox (Gecko) Chrome  Internet Explorer   Opera   Safari
      //Basic support 3.6 7   10                      12.02   6.0.2
      var fileReader = null
      try {
        fileReader = new FileReader() //分块读取文件对象
      } catch (e) {
        if (cbErr) {
          cbErr(tool.getReturnError('当前浏览器不支持FileReader', -18))
          return
        }
      }
      //file的slice方法，注意它的兼容性，在不同浏览器的写法不同
      var blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice
      if (!blobSlice) {
        if (cbErr) {
          cbErr(tool.getReturnError('当前浏览器不支持FileAPI', -19))
          return
        }
      }

      var chunkSize = 2 * 1024 * 1024 //分块大小，2M
      var chunks = Math.ceil(file.size / chunkSize) //总块数
      var currentChunk = 0 //当前块数
      var spark = new SparkMD5() //获取MD5对象

      fileReader.onload = function(e) {
        //数据加载完毕事件

        var binaryStr = ''
        var bytes = new Uint8Array(e.target.result)
        var length = bytes.byteLength
        for (var i = 0; i < length; i++) {
          binaryStr += String.fromCharCode(bytes[i]) //二进制转换字符串
        }
        spark.appendBinary(binaryStr)
        currentChunk++
        if (currentChunk < chunks) {
          loadNext() //读取下一块数据
        } else {
          this.fileMd5 = spark.end() //得到文件MD5值
          if (cbOk) {
            cbOk(this.fileMd5)
          }
        }
      }
      //分片读取文件

      function loadNext() {
        var start = currentChunk * chunkSize,
          end = start + chunkSize >= file.size ? file.size : start + chunkSize
        //根据开始和结束位置，切割文件
        var b = blobSlice.call(file, start, end)
        //readAsBinaryString ie浏览器不兼容此方法
        //fileReader.readAsBinaryString(blobSlice.call(file, start, end));
        fileReader.readAsArrayBuffer(b) //ie，chrome，firefox等主流浏览器兼容此方法
      }

      loadNext() //开始读取
    }
    //提交上传图片表单(用于低版本IE9以下)
    this.submitUploadFileForm = function(options, cbOk, cbErr) {
      var errInfo
      var error
      var formId = options.formId
      var fileId = options.fileId
      var iframeNum = uploadResultIframeId++
      var iframeName = 'uploadResultIframe_' + iframeNum
      var toAccount = options.To_Account
      var businessType = options.businessType

      var form = document.getElementById(formId)
      if (!form) {
        errInfo = '获取表单对象为空: formId=' + formId + '(formId非法)'
        error = tool.getReturnError(errInfo, -20)
        if (cbErr) cbErr(error)
        return
      }

      var fileObj = document.getElementById(fileId)
      if (!fileObj) {
        errInfo = '获取文件对象为空: fileId=' + fileId + '(没有选择文件或者fileId非法)'
        error = tool.getReturnError(errInfo, -21)
        if (cbErr) cbErr(error)
        return
      }
      //fileObj.type="file";//ie8下不起作用，必须由业务自己设置
      fileObj.name = 'file'

      var iframe = document.createElement('iframe')
      iframe.name = iframeName
      iframe.id = iframeName
      iframe.style.display = 'none'
      document.body.appendChild(iframe)

      var cmdName
      if (isAccessFormalEnv()) {
        cmdName = 'pic_up'
      } else {
        cmdName = 'pic_up_test'
      }
      var uploadApiUrl =
        'https://pic.tim.qq.com/v4/openpic/' +
        cmdName +
        '?tinyid=' +
        ctx.tinyid +
        '&a2=' +
        ctx.a2 +
        '&sdkappid=' +
        ctx.sdkAppID +
        '&accounttype=' +
        ctx.accountType +
        '&contenttype=http'
      form.action = uploadApiUrl
      form.method = 'post'
      //form.enctype='multipart/form-data';//ie8下不起作用，必须由业务自己设置
      form.target = iframeName

      function createFormInput(name, value) {
        var tempInput = document.createElement('input')
        tempInput.type = 'hidden'
        tempInput.name = name
        tempInput.value = value
        form.appendChild(tempInput)
      }

      createFormInput('App_Version', VERSION_INFO.APP_VERSION)
      createFormInput('From_Account', ctx.identifier)
      createFormInput('To_Account', toAccount)
      createFormInput('Seq', nextSeq().toString())
      createFormInput('Timestamp', unixtime().toString())
      createFormInput('Random', createRandom().toString())
      createFormInput('Busi_Id', businessType)
      createFormInput('PkgFlag', UPLOAD_RES_PKG_FLAG.RAW_DATA.toString())
      createFormInput('Auth_Key', authkey)
      createFormInput('Server_Ver', VERSION_INFO.SERVER_VERSION.toString())
      createFormInput('File_Type', options.fileType)

      //检测iframe.contentWindow.name是否有值

      function checkFrameName() {
        var resp
        try {
          resp = JSON.parse(iframe.contentWindow.name) || {}
        } catch (e) {
          resp = {}
        }
        if (resp.ActionStatus) {
          //上传接口返回
          // We've got what we need. Stop the iframe from _loading further content.
          iframe.src = 'about:blank'
          iframe.parentNode.removeChild(iframe)
          iframe = null

          if (resp.ActionStatus == ACTION_STATUS.OK) {
            cbOk && cbOk(resp)
          } else {
            cbErr && cbErr(resp)
          }
        } else {
          setTimeout(checkFrameName, 100)
        }
      }

      setTimeout(checkFrameName, 500)

      form.submit() //提交上传图片表单
    }
    //上传图片或文件(用于高版本浏览器，支持FileAPI)
    this.uploadFile = function(options, cbOk, cbErr) {
      var file_upload = {
        //初始化
        init: function(options, cbOk, cbErr) {
          var me = this
          me.file = options.file
          //分片上传进度回调事件
          me.onProgressCallBack = options.onProgressCallBack
          //停止上传图片按钮
          if (options.abortButton) {
            options.abortButton.onclick = me.abortHandler
          }
          me.total = me.file.size //文件总大小
          me.loaded = 0 //已读取字节数
          me.step = 1080 * 1024 //分块大小，1080K
          me.sliceSize = 0 //分片大小
          me.sliceOffset = 0 //当前分片位置
          me.timestamp = unixtime() //当前时间戳
          me.seq = nextSeq() //请求seq
          me.random = createRandom() //请求随机数
          me.fromAccount = ctx.identifier //发送者
          me.toAccount = options.To_Account //接收者
          me.fileMd5 = options.fileMd5 //文件MD5
          me.businessType = options.businessType //图片或文件的业务类型，群消息:1; c2c消息:2; 个人头像：3; 群头像：4;
          me.fileType = options.fileType //文件类型，不填为默认认为上传的是图片；1：图片；2：文件；3：短视频；4：PTT

          me.cbOk = cbOk //上传成功回调事件
          me.cbErr = cbErr //上传失败回调事件

          me.reader = new FileReader() //读取文件对象
          me.blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice //file的slice方法,不同浏览器不一样

          me.reader.onloadstart = me.onLoadStart //开始读取回调事件
          me.reader.onprogress = me.onProgress //读取文件进度回调事件
          me.reader.onabort = me.onAbort //停止读取回调事件
          me.reader.onerror = me.onerror //读取发生错误回调事件
          me.reader.onload = me.onLoad //分片加载完毕回调事件
          me.reader.onloadend = me.onLoadEnd //读取文件完毕回调事件
        },
        //上传方法
        upload: function() {
          var me = file_upload
          //读取第一块
          me.readBlob(0)
        },
        onLoadStart: function() {
          var me = file_upload
        },
        onProgress: function(e) {
          var me = file_upload
          me.loaded += e.loaded
          if (me.onProgressCallBack) {
            me.onProgressCallBack(me.loaded, me.total)
          }
        },
        onAbort: function() {
          var me = file_upload
        },
        onError: function() {
          var me = file_upload
        },
        onLoad: function(e) {
          var me = file_upload
          if (e.target.readyState == FileReader.DONE) {
            var slice_data_base64 = e.target.result
            //注意，一定要去除base64编码头部
            var pos = slice_data_base64.indexOf(',')
            if (pos != -1) {
              slice_data_base64 = slice_data_base64.substr(pos + 1)
            }
            //封装上传图片接口的请求参数
            var opt = {
              From_Account: me.fromAccount,
              To_Account: me.toAccount,
              Busi_Id: me.businessType,
              File_Type: me.fileType,
              File_Str_Md5: me.fileMd5,
              PkgFlag: UPLOAD_RES_PKG_FLAG.BASE64_DATA,
              File_Size: me.total,
              Slice_Offset: me.sliceOffset,
              Slice_Size: me.sliceSize,
              Slice_Data: slice_data_base64,
              Seq: me.seq,
              Timestamp: me.timestamp,
              Random: me.random
            }

            //上传成功的成功回调
            var succCallback = function(resp) {
              if (resp.IsFinish == 0) {
                me.loaded = resp.Next_Offset
                if (me.loaded < me.total) {
                  me.readBlob(me.loaded)
                } else {
                  me.loaded = me.total
                }
              } else {
                if (me.cbOk) {
                  var tempResp = {
                    ActionStatus: resp.ActionStatus,
                    ErrorCode: resp.ErrorCode,
                    ErrorInfo: resp.ErrorInfo,
                    File_UUID: resp.File_UUID,
                    File_Size: resp.Next_Offset,
                    URL_INFO: resp.URL_INFO,
                    Download_Flag: resp.Download_Flag
                  }
                  if (me.fileType == UPLOAD_RES_TYPE.FILE) {
                    //如果上传的是文件，下载地址需要sdk内部拼接
                    tempResp.URL_INFO = getFileDownUrl(resp.File_UUID, ctx.identifier, me.file.name)
                  }
                  me.cbOk(tempResp)
                }
              }
              Upload_Retry_Times = 0
            }
            //上传失败的回调
            var errorCallback = function(resp) {
              if (Upload_Retry_Times < Upload_Retry_Max_Times) {
                Upload_Retry_Times++
                setTimeout(function() {
                  proto_uploadPic(opt, succCallback, errorCallback)
                }, 1000)
              } else {
                me.cbErr(resp)
              }
              //me.cbErr
            }
            //分片上传图片接口
            proto_uploadPic(opt, succCallback, errorCallback)
          }
        },
        onLoadEnd: function() {
          var me = file_upload
        },
        //分片读取文件方法
        readBlob: function(start) {
          var me = file_upload
          var blob,
            file = me.file
          var end = start + me.step
          if (end > me.total) {
            end = me.total
            me.sliceSize = end - start
          } else {
            me.sliceSize = me.step
          }
          me.sliceOffset = start
          //根据起始和结束位置，分片读取文件
          blob = me.blobSlice.call(file, start, end)
          //将分片的二进制数据转换为base64编码
          me.reader.readAsDataURL(blob)
        },
        abortHandler: function() {
          var me = file_upload
          if (me.reader) {
            me.reader.abort()
          }
        }
      }

      //读取文件MD5
      getFileMD5(
        options.file,
        function(fileMd5) {
          log.info('fileMd5: ' + fileMd5)
          options.fileMd5 = fileMd5
          //初始化上传参数
          file_upload.init(options, cbOk, cbErr)
          //开始上传文件
          file_upload.upload()
        },
        cbErr
      )
    }
  }()

  //web im 基础对象

  //常量对象

  //会话类型
  webim.SESSION_TYPE = SESSION_TYPE

  webim.MSG_MAX_LENGTH = MSG_MAX_LENGTH

  //c2c消息子类型
  webim.C2C_MSG_SUB_TYPE = C2C_MSG_SUB_TYPE

  //群消息子类型
  webim.GROUP_MSG_SUB_TYPE = GROUP_MSG_SUB_TYPE

  //消息元素类型
  webim.MSG_ELEMENT_TYPE = MSG_ELEMENT_TYPE

  //群提示消息类型
  webim.GROUP_TIP_TYPE = GROUP_TIP_TYPE

  //图片类型
  webim.IMAGE_TYPE = IMAGE_TYPE

  //群系统消息类型
  webim.GROUP_SYSTEM_TYPE = GROUP_SYSTEM_TYPE

  //好友系统通知子类型
  webim.FRIEND_NOTICE_TYPE = FRIEND_NOTICE_TYPE

  //群提示消息-群资料变更类型
  webim.GROUP_TIP_MODIFY_GROUP_INFO_TYPE = GROUP_TIP_MODIFY_GROUP_INFO_TYPE

  //浏览器信息
  webim.BROWSER_INFO = BROWSER_INFO

  //表情对象
  webim.Emotions = webim.EmotionPicData = emotions
  //表情标识符和index Map
  webim.EmotionDataIndexs = webim.EmotionPicDataIndex = emotionDataIndexs

  //腾讯登录服务错误码(托管模式)
  webim.TLS_ERROR_CODE = TLS_ERROR_CODE

  //连接状态
  webim.CONNECTION_STATUS = CONNECTION_STATUS

  //上传图片业务类型
  webim.UPLOAD_PIC_BUSSINESS_TYPE = UPLOAD_PIC_BUSSINESS_TYPE

  //最近联系人类型
  webim.RECENT_CONTACT_TYPE = RECENT_CONTACT_TYPE

  //上传资源类型
  webim.UPLOAD_RES_TYPE = UPLOAD_RES_TYPE

  /**************************************/

  //类对象
  //
  //工具对象
  webim.Tool = tool
  //控制台打印日志对象
  webim.Log = log

  //消息对象
  webim.Msg = Msg
  //会话对象
  webim.Session = Session
  //会话存储对象
  webim.MsgStore = {
    sessMap: function() {
      return MsgStore.sessMap()
    },
    sessCount: function() {
      return MsgStore.sessCount()
    },
    sessByTypeId: function(type, id) {
      return MsgStore.sessByTypeId(type, id)
    },
    delSessByTypeId: function(type, id) {
      return MsgStore.delSessByTypeId(type, id)
    },
    resetCookieAndSyncFlag: function() {
      return MsgStore.resetCookieAndSyncFlag()
    }
  }

  webim.Resources = Resources

  /**************************************/

  // webim API impl
  //
  //基本接口
  //登录
  webim.login = webim.init = function(loginInfo, listeners, opts, cbOk, cbErr) {
    //初始化连接状态回调函数
    ConnManager.init(listeners.onConnNotify, cbOk, cbErr)

    //设置ie9以下浏览器jsonp回调
    if (listeners.jsonpCallback) jsonpCallback = listeners.jsonpCallback
    //登录
    _login(loginInfo, listeners, opts, cbOk, cbErr)
  }
  //登出
  //需要传长轮询id
  //这样登出之后其他的登录实例还可以继续收取消息
  webim.logout = webim.offline = function(cbOk, cbErr) {
    return proto_logout('instance', cbOk, cbErr)
  }

  //登出
  //这种登出方式，所有的实例都将不会收到消息推送，直到重新登录
  webim.logoutAll = function(cbOk, cbErr) {
    return proto_logout('all', cbOk, cbErr)
  }

  //消息管理接口
  //发消息接口（私聊和群聊）
  webim.sendMsg = function(msg, cbOk, cbErr) {
    return MsgManager.sendMsg(msg, cbOk, cbErr)
  }
  //拉取未读c2c消息
  webim.syncMsgs = function(cbOk, cbErr) {
    return MsgManager.syncMsgs(cbOk, cbErr)
  }
  //拉取C2C漫游消息
  webim.getC2CHistoryMsgs = function(options, cbOk, cbErr) {
    return MsgManager.getC2CHistoryMsgs(options, cbOk, cbErr)
  }
  //拉取群漫游消息
  webim.syncGroupMsgs = function(options, cbOk, cbErr) {
    return MsgManager.syncGroupMsgs(options, cbOk, cbErr)
  }

  //上报c2c消息已读
  webim.c2CMsgReaded = function(options, cbOk, cbErr) {
    return MsgStore.c2CMsgReaded(options, cbOk, cbErr)
  }

  //上报群消息已读
  webim.groupMsgReaded = function(options, cbOk, cbErr) {
    return proto_groupMsgReaded(options, cbOk, cbErr)
  }

  //设置聊天会话自动标记已读
  webim.setAutoRead = function(selSess, isOn, isResetAll) {
    return MsgStore.setAutoRead(selSess, isOn, isResetAll)
  }

  //群组管理接口
  //
  //创建群
  webim.createGroup = function(options, cbOk, cbErr) {
    return proto_createGroup(options, cbOk, cbErr)
  }
  //创建群-高级接口
  webim.createGroupHigh = function(options, cbOk, cbErr) {
    return proto_createGroupHigh(options, cbOk, cbErr)
  }
  //申请加群
  webim.applyJoinGroup = function(options, cbOk, cbErr) {
    return proto_applyJoinGroup(options, cbOk, cbErr)
  }
  //处理加群申请(同意或拒绝)
  webim.handleApplyJoinGroupPendency = function(options, cbOk, cbErr) {
    return proto_handleApplyJoinGroupPendency(options, cbOk, cbErr)
  }

  //获取群组未决列表
  webim.getPendencyGroup = function(options, cbOk, cbErr) {
    return proto_getPendencyGroup(options, cbOk, cbErr)
  }

  //群未决已读上报
  webim.getPendencyGroupRead = function(options, cbOk, cbErr) {
    return proto_getPendencyGroupRead(options, cbOk, cbErr)
  }

  //处理邀请进群申请(同意或拒绝)
  webim.handleInviteJoinGroupRequest = function(options, cbOk, cbErr) {
    return proto_handleInviteJoinGroupRequest(options, cbOk, cbErr)
  }

  //删除加群申请
  webim.deleteApplyJoinGroupPendency = function(options, cbOk, cbErr) {
    return proto_deleteC2CMsg(options, cbOk, cbErr)
  }

  //主动退群
  webim.quitGroup = function(options, cbOk, cbErr) {
    return proto_quitGroup(options, cbOk, cbErr)
  }
  //搜索群组(根据名称)
  webim.searchGroupByName = function(options, cbOk, cbErr) {
    return proto_searchGroupByName(options, cbOk, cbErr)
  }
  //获取群组公开资料(根据群id搜索)
  webim.getGroupPublicInfo = function(options, cbOk, cbErr) {
    return proto_getGroupPublicInfo(options, cbOk, cbErr)
  }
  //获取群组详细资料-高级接口
  webim.getGroupInfo = function(options, cbOk, cbErr) {
    return proto_getGroupInfo(options, cbOk, cbErr)
  }
  //修改群基本资料
  webim.modifyGroupBaseInfo = function(options, cbOk, cbErr) {
    return proto_modifyGroupBaseInfo(options, cbOk, cbErr)
  }
  //获取群成员列表
  webim.getGroupMemberInfo = function(options, cbOk, cbErr) {
    return proto_getGroupMemberInfo(options, cbOk, cbErr)
  }
  //邀请好友加群
  webim.addGroupMember = function(options, cbOk, cbErr) {
    return proto_addGroupMember(options, cbOk, cbErr)
  }
  //修改群成员资料
  webim.modifyGroupMember = function(options, cbOk, cbErr) {
    return proto_modifyGroupMember(options, cbOk, cbErr)
  }
  //删除群成员
  webim.deleteGroupMember = function(options, cbOk, cbErr) {
    return proto_deleteGroupMember(options, cbOk, cbErr)
  }
  //解散群
  webim.destroyGroup = function(options, cbOk, cbErr) {
    return proto_destroyGroup(options, cbOk, cbErr)
  }
  //转让群组
  webim.changeGroupOwner = function(options, cbOk, cbErr) {
    return proto_changeGroupOwner(options, cbOk, cbErr)
  }

  //获取我的群组列表-高级接口
  webim.getJoinedGroupListHigh = function(options, cbOk, cbErr) {
    return proto_getJoinedGroupListHigh(options, cbOk, cbErr)
  }
  //获取群成员角色
  webim.getRoleInGroup = function(options, cbOk, cbErr) {
    return proto_getRoleInGroup(options, cbOk, cbErr)
  }
  //设置群成员禁言时间
  webim.forbidSendMsg = function(options, cbOk, cbErr) {
    return proto_forbidSendMsg(options, cbOk, cbErr)
  }
  //发送自定义群系统通知
  webim.sendCustomGroupNotify = function(options, cbOk, cbErr) {
    return proto_sendCustomGroupNotify(options, cbOk, cbErr)
  }

  //进入大群
  webim.applyJoinBigGroup = function(options, cbOk, cbErr) {
    return proto_applyJoinBigGroup(options, cbOk, cbErr)
  }
  //退出大群
  webim.quitBigGroup = function(options, cbOk, cbErr) {
    return proto_quitBigGroup(options, cbOk, cbErr)
  }

  //资料关系链管理接口
  //
  //获取个人资料接口，可用于搜索用户
  webim.getProfilePortrait = function(options, cbOk, cbErr) {
    return proto_getProfilePortrait(options, cbOk, cbErr)
  }
  //设置个人资料
  webim.setProfilePortrait = function(options, cbOk, cbErr) {
    return proto_setProfilePortrait(options, cbOk, cbErr)
  }
  //申请加好友
  webim.applyAddFriend = function(options, cbOk, cbErr) {
    return proto_applyAddFriend(options, cbOk, cbErr)
  }
  //获取好友申请列表
  webim.getPendency = function(options, cbOk, cbErr) {
    return proto_getPendency(options, cbOk, cbErr)
  }
  //好友申请列表已读上报
  webim.getPendencyReport = function(options, cbOk, cbErr) {
    return proto_getPendencyReport(options, cbOk, cbErr)
  }
  //删除好友申请
  webim.deletePendency = function(options, cbOk, cbErr) {
    return proto_deletePendency(options, cbOk, cbErr)
  }
  //处理好友申请
  webim.responseFriend = function(options, cbOk, cbErr) {
    return proto_responseFriend(options, cbOk, cbErr)
  }
  //获取我的好友
  webim.getAllFriend = function(options, cbOk, cbErr) {
    return proto_getAllFriend(options, cbOk, cbErr)
  }
  //删除会话
  webim.deleteChat = function(options, cbOk, cbErr) {
    return proto_deleteChat(options, cbOk, cbErr)
  }
  //删除好友
  webim.deleteFriend = function(options, cbOk, cbErr) {
    return proto_deleteFriend(options, cbOk, cbErr)
  }
  //拉黑
  webim.addBlackList = function(options, cbOk, cbErr) {
    return proto_addBlackList(options, cbOk, cbErr)
  }
  //删除黑名单
  webim.deleteBlackList = function(options, cbOk, cbErr) {
    return proto_deleteBlackList(options, cbOk, cbErr)
  }
  //获取我的黑名单
  webim.getBlackList = function(options, cbOk, cbErr) {
    return proto_getBlackList(options, cbOk, cbErr)
  }

  //获取最近会话
  webim.getRecentContactList = function(options, cbOk, cbErr) {
    return proto_getRecentContactList(options, cbOk, cbErr)
  }

  //图片或文件服务接口
  //
  //上传文件接口（高版本浏览器）
  webim.uploadFile = webim.uploadPic = function(options, cbOk, cbErr) {
    return FileUploader.uploadFile(options, cbOk, cbErr)
  }
  //提交上传图片表单接口（用于低版本ie）
  webim.submitUploadFileForm = function(options, cbOk, cbErr) {
    return FileUploader.submitUploadFileForm(options, cbOk, cbErr)
  }
  //上传图片或文件(Base64)接口
  webim.uploadFileByBase64 = webim.uploadPicByBase64 = function(options, cbOk, cbErr) {
    //请求参数
    var opt = {
      To_Account: options.toAccount,
      Busi_Id: options.businessType,
      File_Type: options.File_Type,
      File_Str_Md5: options.fileMd5,
      PkgFlag: UPLOAD_RES_PKG_FLAG.BASE64_DATA,
      File_Size: options.totalSize,
      Slice_Offset: 0,
      Slice_Size: options.totalSize,
      Slice_Data: options.base64Str,
      Seq: nextSeq(),
      Timestamp: unixtime(),
      Random: createRandom()
    }
    return proto_uploadPic(opt, cbOk, cbErr)
  }

  //设置jsonp返回的值
  webim.setJsonpLastRspData = function(rspData) {
    jsonpLastRspData = typeof rspData == 'string' ? JSON.parse(rspData) : rspData
  }

  //获取长轮询ID
  webim.getLongPollingId = function(options, cbOk, cbErr) {
    return proto_getLongPollingId(options, cbOk, cbErr)
  }

  //获取下载地址
  webim.applyDownload = function(options, cbOk, cbErr) {
    return proto_applyDownload(options, cbOk, cbErr)
  }

  //获取下载地址
  webim.onDownFile = function(uuid) {
    window.open(Resources.downloadMap['uuid_' + uuid])
  }

  //检查是否登录
  webim.checkLogin = function(cbErr, isNeedCallBack) {
    return checkLogin(cbErr, isNeedCallBack)
  }
})(webim)

export default webim
